From 67f67a67b9ff8cdab54954bfc0b628edd614c033 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 11 Aug 2014 16:42:38 -0700 Subject: [PATCH 001/952] v3.9.7.pre --- Gemfile.lock | 14 +++++++------- heroku.gemspec | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c41b2a239..c2a821dd5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - heroku (3.9.4) - heroku-api (= 0.3.17) + heroku (3.9.7.pre) + heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) rest-client (~> 1.6.1) @@ -25,7 +25,7 @@ GEM clamp (0.5.0) crack (0.3.2) diff-lcs (1.1.3) - excon (0.38.0) + excon (0.39.4) fakefs (0.4.2) fpm (0.4.6) arr-pm (~> 0.0.7) @@ -33,14 +33,14 @@ GEM cabin (~> 0.4.3) clamp json - heroku-api (0.3.17) - excon (~> 0.27) - multi_json (~> 1.8.2) + heroku-api (0.3.19) + excon (~> 0.38) + multi_json (~> 1.8) json (1.7.7) launchy (2.4.2) addressable (~> 2.3) mime-types (1.21) - multi_json (1.8.4) + multi_json (1.10.1) netrc (0.7.7) rake (10.0.3) rdoc (4.1.1) diff --git a/heroku.gemspec b/heroku.gemspec index 650db705e..fdae16dab 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(License|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", "= 0.3.17" + gem.add_dependency "heroku-api", "~> 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", "~> 0.7.7" gem.add_dependency "rest-client", "~> 1.6.1" diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 43b86f551..0e0809839 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.4" + VERSION = "3.9.7.pre" end From 7824eed18f62b73e10c2f1f71c40f105aee89082 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 11 Aug 2014 17:03:59 -0700 Subject: [PATCH 002/952] v3.9.7.pre2 --- lib/heroku/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0e0809839..0a8c19b65 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.7.pre" + VERSION = "3.9.7.pre2" end From f6389f4486afa5f15f804dc7b516ad7440bc562f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 11 Aug 2014 17:17:40 -0700 Subject: [PATCH 003/952] v3.9.7.pre2 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c2a821dd5..e295f2877 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.9.7.pre) + heroku (3.9.7.pre2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) From 62aea406412619f3e8f744906e85a6c27e9e90d1 Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Tue, 12 Aug 2014 18:14:27 +0200 Subject: [PATCH 004/952] remove debug statement --- lib/heroku/command/pg.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index ce46abee7..754969959 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -341,7 +341,7 @@ def pull def maintenance mode_with_argument = shift_argument || '' mode, mode_argument = mode_with_argument.split('=') - p [mode, mode_argument] + db = shift_argument no_maintenance = options[:force] if mode.nil? || db.nil? || !(%w[info run window].include? mode) From be8d8cfe43e2010cf235a6f7d2f0c6183b3b3f3a Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Tue, 12 Aug 2014 18:15:01 +0200 Subject: [PATCH 005/952] lock versions of heroku-api, excon and rest-client --- Gemfile.lock | 20 +++++++++----------- heroku.gemspec | 5 +++-- lib/heroku/version.rb | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e295f2877..cd6460eeb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,11 +1,12 @@ PATH remote: . specs: - heroku (3.9.7.pre2) - heroku-api (~> 0.3.19) + heroku (3.9.7.pre4) + excon (= 0.33.0) + heroku-api (= 0.3.18) launchy (>= 0.3.2) netrc (~> 0.7.7) - rest-client (~> 1.6.1) + rest-client (= 1.6.7) rubyzip GEM @@ -25,7 +26,7 @@ GEM clamp (0.5.0) crack (0.3.2) diff-lcs (1.1.3) - excon (0.39.4) + excon (0.33.0) fakefs (0.4.2) fpm (0.4.6) arr-pm (~> 0.0.7) @@ -33,8 +34,8 @@ GEM cabin (~> 0.4.3) clamp json - heroku-api (0.3.19) - excon (~> 0.38) + heroku-api (0.3.18) + excon (~> 0.27) multi_json (~> 1.8) json (1.7.7) launchy (2.4.2) @@ -43,11 +44,8 @@ GEM multi_json (1.10.1) netrc (0.7.7) rake (10.0.3) - rdoc (4.1.1) - json (~> 1.4) - rest-client (1.6.8) - mime-types (~> 1.16) - rdoc (>= 2.4.2) + rest-client (1.6.7) + mime-types (>= 1.16) rr (1.0.4) rspec (2.12.0) rspec-core (~> 2.12.0) diff --git a/heroku.gemspec b/heroku.gemspec index fdae16dab..542f5b76c 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -20,9 +20,10 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(License|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", "~> 0.3.19" + gem.add_dependency "excon", "= 0.33.0" + gem.add_dependency "heroku-api", "= 0.3.18" gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", "~> 0.7.7" - gem.add_dependency "rest-client", "~> 1.6.1" + gem.add_dependency "rest-client", "= 1.6.7" gem.add_dependency "rubyzip" end diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0a8c19b65..7b3b03651 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.7.pre2" + VERSION = "3.9.7.pre4" end From 4a9bb3a1bdbf53ac29547407c9c1af591f71a33b Mon Sep 17 00:00:00 2001 From: Phil Hagelberg Date: Tue, 12 Aug 2014 09:27:56 -0700 Subject: [PATCH 006/952] Don't update manifest or update hash during beta phase. --- dist/manifest.rake | 2 ++ dist/zip.rake | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dist/manifest.rake b/dist/manifest.rake index 0d3b46e0e..adaf03b04 100644 --- a/dist/manifest.rake +++ b/dist/manifest.rake @@ -1,4 +1,6 @@ task "manifest:update" do + abort "Manifest should never contain betas." if beta? + tempdir do |dir| File.open("VERSION", "w") do |file| file.puts version diff --git a/dist/zip.rake b/dist/zip.rake index 9f7252c23..bc3a98af2 100644 --- a/dist/zip.rake +++ b/dist/zip.rake @@ -36,5 +36,5 @@ task "zip:release" => %w( zip:build zip:sign ) do |t| store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client-beta.zip" if beta? store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client.zip" unless beta? - sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" + sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" unless beta? end From f415823a113bead6a2fef0be7c98839395dec20b Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Tue, 12 Aug 2014 18:49:13 +0200 Subject: [PATCH 007/952] v3.7.9 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 50c19b84b..873d58836 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.9.7 2014-08-12 +================ +Bring 2fa:disable back +Several pg and pgbackups command improvements + 3.9.4 2014-07-21 ================ Actually fix a bug where setting HEROKU_API_KEY would cause failures diff --git a/Gemfile.lock b/Gemfile.lock index cd6460eeb..b1e23f425 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.9.7.pre4) + heroku (3.9.7) excon (= 0.33.0) heroku-api (= 0.3.18) launchy (>= 0.3.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 7b3b03651..0272de5e2 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.7.pre4" + VERSION = "3.9.7" end From 8af6024a5e60e5be7ba6fc7c1111f51993d82694 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 12 Aug 2014 13:47:05 -0700 Subject: [PATCH 008/952] upgraded heroku-api and excon --- Gemfile.lock | 9 ++++----- heroku.gemspec | 3 +-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b1e23f425..4400f764b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,8 +2,7 @@ PATH remote: . specs: heroku (3.9.7) - excon (= 0.33.0) - heroku-api (= 0.3.18) + heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) rest-client (= 1.6.7) @@ -26,7 +25,7 @@ GEM clamp (0.5.0) crack (0.3.2) diff-lcs (1.1.3) - excon (0.33.0) + excon (0.39.4) fakefs (0.4.2) fpm (0.4.6) arr-pm (~> 0.0.7) @@ -34,8 +33,8 @@ GEM cabin (~> 0.4.3) clamp json - heroku-api (0.3.18) - excon (~> 0.27) + heroku-api (0.3.19) + excon (~> 0.38) multi_json (~> 1.8) json (1.7.7) launchy (2.4.2) diff --git a/heroku.gemspec b/heroku.gemspec index 542f5b76c..ddf4a582d 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -20,8 +20,7 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(License|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "excon", "= 0.33.0" - gem.add_dependency "heroku-api", "= 0.3.18" + gem.add_dependency "heroku-api", "~> 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", "~> 0.7.7" gem.add_dependency "rest-client", "= 1.6.7" From ae90ba9bdeae4a5ecf156ec1e3c61ef79a7cdd8f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 12 Aug 2014 13:48:47 -0700 Subject: [PATCH 009/952] v3.9.8.pre --- CHANGELOG | 4 ++++ lib/heroku/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 873d58836..da80a07ce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.9.8 2014-08-13 +================ +Upgrade heroku-api and excon + 3.9.7 2014-08-12 ================ Bring 2fa:disable back diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0272de5e2..432752276 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.7" + VERSION = "3.9.8.pre" end From cafeff525bae83608b8a3eac9eb1cb4ac2d9b1ce Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Tue, 12 Aug 2014 14:50:20 -0700 Subject: [PATCH 010/952] Better errors when psql fails and when we can't check the pg version --- lib/heroku/command/pg.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 754969959..563533025 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -530,7 +530,9 @@ def find_uri def version return @version if defined? @version - @version = exec_sql("select version();").match(/PostgreSQL (\d+\.\d+\.\d+) on/)[1] + result = exec_sql("select version();").match(/PostgreSQL (\d+\.\d+\.\d+) on/) + fail("Unable to determine Postgres version") unless result + @version = result[1] end def nine_two? @@ -564,7 +566,11 @@ def exec_sql_on_uri(sql,uri) ENV["PGPASSWORD"] = uri.password ENV["PGSSLMODE"] = (uri.host == 'localhost' ? 'prefer' : 'require' ) user_part = uri.user ? "-U #{uri.user}" : "" - `psql -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` + output = `psql -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` + if (! $?.success?) || output.nil? || output.empty? + raise "psql failed. exit status #{$?.to_i}, output: #{output.inspect}" + end + output rescue Errno::ENOENT output_with_bang "The local psql command could not be located" output_with_bang "For help installing psql, see https://devcenter.heroku.com/articles/heroku-postgresql#local-setup" From cafe066822e9dfa43213e0c318a498590925c145 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Wed, 13 Aug 2014 09:28:37 -0700 Subject: [PATCH 011/952] ingore psql aliases in exec_sql --- lib/heroku/command/pg.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 563533025..a25e2faed 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -566,7 +566,7 @@ def exec_sql_on_uri(sql,uri) ENV["PGPASSWORD"] = uri.password ENV["PGSSLMODE"] = (uri.host == 'localhost' ? 'prefer' : 'require' ) user_part = uri.user ? "-U #{uri.user}" : "" - output = `psql -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` + output = `#{psql_cmd} -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` if (! $?.success?) || output.nil? || output.empty? raise "psql failed. exit status #{$?.to_i}, output: #{output.inspect}" end @@ -578,4 +578,9 @@ def exec_sql_on_uri(sql,uri) end end + def psql_cmd + # some people alais psql, so we need to find the real psql + # but windows doesn't have the command command + running_on_windows? ? 'psql' : 'command psql' + end end From f8bafc5ff970f4f4cf5bec9c77f0bd4b08d5ea8b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 13:44:15 -0700 Subject: [PATCH 012/952] use svg badges for retina displays --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 775604fc3..69aadedd3 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ For more about Heroku see . To get started see -[![Build Status](https://secure.travis-ci.org/heroku/heroku.png)](http://travis-ci.org/heroku/heroku) -[![Dependency Status](https://gemnasium.com/heroku/heroku.png)](https://gemnasium.com/heroku/heroku) +[![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) +[![Dependency Status](https://gemnasium.com/heroku/heroku.svg)](https://gemnasium.com/heroku/heroku) Setup ----- From 04dc0ad00701ff17aaacd66111a1aba1899b1db8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 13:11:46 -0700 Subject: [PATCH 013/952] fixed beta releases --- lib/heroku/command/update.rb | 8 +- lib/heroku/updater.rb | 107 +++++++++++++------------ spec/fixtures/heroku-client-3.9.7.zip | Bin 0 -> 852640 bytes spec/heroku/updater_spec.rb | 111 +++++++++++++++++++++----- 4 files changed, 151 insertions(+), 75 deletions(-) create mode 100644 spec/fixtures/heroku-client-3.9.7.zip diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 67b977a15..5a7ca34ba 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -16,7 +16,7 @@ class Heroku::Command::Update < Heroku::Command::Base # def index validate_arguments! - update_from_url("https://toolbelt.heroku.com/download/zip") + update_from_url(false) end # update:beta @@ -28,15 +28,15 @@ def index # def beta validate_arguments! - update_from_url("https://toolbelt.heroku.com/download/beta-zip") + update_from_url(true) end private - def update_from_url(url) + def update_from_url(prerelease) Heroku::Updater.check_disabled! action("Updating from #{Heroku::VERSION}") do - if new_version = Heroku::Updater.update(url) + if new_version = Heroku::Updater.update(prerelease) status("updated to #{new_version}") else status("nothing to update") diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 87540aa8c..d64afe5f4 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -21,6 +21,20 @@ def self.updated_client_path File.join(Heroku::Helpers.home_directory, ".heroku", "client") end + def self.latest_version + http_get('http://assets.heroku.com/heroku-client/VERSION').chomp + end + + def self.official_zip_hash + http_get('https://toolbelt.heroku.com/update/hash').chomp + end + + def self.http_get(url) + require 'excon' + require 'heroku/excon' + Excon.get_with_redirect(url, :nonblock => false).body + end + def self.latest_local_version installed_version = client_version_from_path(installed_client_path) updated_version = client_version_from_path(updated_client_path) @@ -31,6 +45,10 @@ def self.latest_local_version end end + def self.needs_update? + compare_versions(latest_version, latest_local_version) > 0 + end + def self.client_version_from_path(path) version_file = File.join(path, "lib/heroku/version.rb") if File.exists?(version_file) @@ -51,7 +69,8 @@ def self.check_disabled! end end - def self.wait_for_lock(path, wait_for=5, check_every=0.5) + def self.wait_for_lock(wait_for=5, check_every=0.5) + path = updating_lock_path start = Time.now.to_i while File.exists?(path) sleep check_every @@ -59,69 +78,59 @@ def self.wait_for_lock(path, wait_for=5, check_every=0.5) Heroku::Helpers.error "Unable to acquire update lock" end end - begin - FileUtils.touch path - ret = yield - ensure - FileUtils.rm_f path - end - ret - end - - def self.autoupdate? - true + FileUtils.mkdir_p File.dirname(path) + FileUtils.touch path + yield + ensure + FileUtils.rm_f path end - def self.update(url, autoupdate=false) - wait_for_lock(updating_lock_path, 5) do - require "excon" + def self.update(prerelease) + wait_for_lock do require "heroku" - require "heroku/excon" require "tmpdir" require "zip/zip" - latest_version = Excon.get_with_redirect("http://assets.heroku.com/heroku-client/VERSION", :nonblock => false).body.chomp - - if compare_versions(latest_version, latest_local_version) > 0 - Dir.mktmpdir do |download_dir| - File.open("#{download_dir}/heroku.zip", "wb") do |file| - file.print Excon.get_with_redirect(url, :nonblock => false).body - end - - hash = Digest::SHA256.file("#{download_dir}/heroku.zip").hexdigest - official_hash = Excon.get_with_redirect("https://toolbelt.heroku.com/update/hash", :nonblock => false).body.chomp + return unless prerelease || needs_update? - error "Update hash signature mismatch" unless hash == official_hash + Dir.mktmpdir do |download_dir| + zip_filename = "#{download_dir}/heroku.zip" + if prerelease + url = "https://toolbelt.heroku.com/download/beta-zip" + else + url = "https://toolbelt.heroku.com/download/zip" + end - Zip::ZipFile.open("#{download_dir}/heroku.zip") do |zip| - zip.each do |entry| - target = File.join(download_dir, entry.to_s) - FileUtils.mkdir_p File.dirname(target) - zip.extract(entry, target) { true } - end - end + download_file(url, zip_filename) + hash = Digest::SHA256.file(zip_filename).hexdigest + error "Update hash signature mismatch" unless hash == official_zip_hash - FileUtils.rm "#{download_dir}/heroku.zip" + extract_zip(zip_filename, download_dir) + FileUtils.rm_f zip_filename - old_version = latest_local_version - new_version = client_version_from_path(download_dir) + FileUtils.rm_rf updated_client_path + FileUtils.mkdir_p File.dirname(updated_client_path) + FileUtils.cp_r download_dir, updated_client_path - if compare_versions(new_version, old_version) < 0 && !autoupdate - Heroku::Helpers.error("Installed version (#{old_version}) is newer than the latest available update (#{new_version})") - end + client_version_from_path(download_dir) + end + end + end - FileUtils.rm_rf updated_client_path - FileUtils.mkdir_p File.dirname(updated_client_path) - FileUtils.cp_r download_dir, updated_client_path + def self.download_file(from_url, to_filename) + File.open(to_filename, "wb") do |file| + file.print http_get(from_url) + end + end - new_version - end - else - false # already up to date + def self.extract_zip(filename, dir) + Zip::ZipFile.open(filename) do |zip| + zip.each do |entry| + target = File.join(dir, entry.to_s) + FileUtils.mkdir_p File.dirname(target) + zip.extract(entry, target) { true } end end - ensure - FileUtils.rm_f(updating_lock_path) end def self.compare_versions(first_version, second_version) diff --git a/spec/fixtures/heroku-client-3.9.7.zip b/spec/fixtures/heroku-client-3.9.7.zip new file mode 100644 index 0000000000000000000000000000000000000000..c109fdcf83aa87c72f8e17f39a9cc4d563d19f31 GIT binary patch literal 852640 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%0DIi4VEJ-46?qlGP`j1b5To>0Xwtd{#B< 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
    C>XG?sFH8w9`_~gU_ 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

    Jy`?Dc+HIub@smOXVrjwh z?v2rC2(d0|eoOQ(_O8!|k59VXexkl>JyxnD03y^)?t6+ORi`(lDuDS=a^)8CoBN7j zsFkBv#X&9uMAb?W!ipuZ-2>jFA&4mLpaB))A@`;dO?d}MJG#}QfXX-v5#2Z*^bCm1 zF0$$so<>)Xwy*X^BK?>xBj346S!=1B2u?E;tEl3fI^}Xu%>Ml`oXJQLAU}KUKI8M+ z*xFw#Vxv(+#ZHOBeOb$XQ;YwK$@f4N*9tVtDGh0^jJzl)kONzMtqtDd_xSpw1;-l^ zc;i8|l|c$kmWM7q<_dr^qRrELsAeO_b@-?oWY1|rdNA;+>s5t?kUzqr(~lZc^D9yT z!Q$b*uMW%)-b~KNsX6a!C9ei&N!>O5*uzpc*)S4*??!5;<4o-cX_Z&5 z@;OjAOisSU^oucICoTIJ%EZtN(3A{e#sax!WvMrfWZ(DG=Sf!@H@fmVY0eS2sH|w5 zU`|ia(!M$SRR>+*bHw+b$eJ`1wO*VGoizS3ce%+g%tv0R`1)@Hi{(>r!pvtS=M#}0D8^=#6De{mO;X! zHR=F-aiBbA9EVrN()r~YW|TSZKsml1APTiI9lj^|j*OxFtTO%w8RrJ)#HtMsu^sF{ zq%Sll0x*p9qTmND7mYt!pm3P=9Bo+Q{-PRN{`=q}CCUm0f?&)wNAcPbw^TXd-mBCp zv|$?k!PFBA=4P$27siiNx*@5M+DLK;QRT*nKuDB(GcJa0k22}9C7#y$0g{~}&9Db~ zb`LE%+>0T(7lNcnKPAD~xv?{%ux~f^@vU5gE^MLD;YSPz97++WP %ZnJ0IasXdq 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 {D({)@~>b$Byt2J01(wNBNW`QC%&k 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�@*JiDZ*QD`ZbtQz~PD=S^ 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�}|qdSs$;WBNzgpX4f05=sa4WEeeh44tf`m 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

    }jn7*42ef7_sBzH9D@znq@YmMH-^c?#l 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

    @<>5Pg|tVfa30@AS) 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 |VD7^vD5f%}H99H}!)803I7%78ca8 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 literal 0 HcmV?d00001 diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index bc1f4be3e..7c256b66e 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -5,40 +5,107 @@ module Heroku describe Updater do - it "calculates the latest local version" do - Heroku::Updater.latest_local_version.should == Heroku::VERSION + describe('::latest_local_version') do + it 'calculates the latest local version' do + subject.latest_local_version.should == Heroku::VERSION + end end - it "calculates compare_versions" do - Heroku::Updater.compare_versions('1.1.1', '1.1.1').should == 0 + describe('::compare_versions') do + it 'calculates compare_versions' do + subject.compare_versions('1.1.1', '1.1.1').should == 0 - Heroku::Updater.compare_versions('2.1.1', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '2.1.1').should == -1 + subject.compare_versions('2.1.1', '1.1.1').should == 1 + subject.compare_versions('1.1.1', '2.1.1').should == -1 - Heroku::Updater.compare_versions('1.2.1', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '1.2.1').should == -1 + subject.compare_versions('1.2.1', '1.1.1').should == 1 + subject.compare_versions('1.1.1', '1.2.1').should == -1 - Heroku::Updater.compare_versions('1.1.2', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '1.1.2').should == -1 + subject.compare_versions('1.1.2', '1.1.1').should == 1 + subject.compare_versions('1.1.1', '1.1.2').should == -1 - Heroku::Updater.compare_versions('2.1.1', '1.2.1').should == 1 - Heroku::Updater.compare_versions('1.2.1', '2.1.1').should == -1 + subject.compare_versions('2.1.1', '1.2.1').should == 1 + subject.compare_versions('1.2.1', '2.1.1').should == -1 - Heroku::Updater.compare_versions('2.1.1', '1.1.2').should == 1 - Heroku::Updater.compare_versions('1.1.2', '2.1.1').should == -1 + subject.compare_versions('2.1.1', '1.1.2').should == 1 + subject.compare_versions('1.1.2', '2.1.1').should == -1 - Heroku::Updater.compare_versions('1.2.4', '1.2.3').should == 1 - Heroku::Updater.compare_versions('1.2.3', '1.2.4').should == -1 + subject.compare_versions('1.2.4', '1.2.3').should == 1 + subject.compare_versions('1.2.3', '1.2.4').should == -1 - Heroku::Updater.compare_versions('1.2.1', '1.2' ).should == 1 - Heroku::Updater.compare_versions('1.2', '1.2.1').should == -1 + subject.compare_versions('1.2.1', '1.2' ).should == 1 + subject.compare_versions('1.2', '1.2.1').should == -1 - Heroku::Updater.compare_versions('1.1.1.pre1', '1.1.1').should == 1 - Heroku::Updater.compare_versions('1.1.1', '1.1.1.pre1').should == -1 + subject.compare_versions('1.1.1.pre1', '1.1.1').should == 1 + subject.compare_versions('1.1.1', '1.1.1.pre1').should == -1 - Heroku::Updater.compare_versions('1.1.1.pre2', '1.1.1.pre1').should == 1 - Heroku::Updater.compare_versions('1.1.1.pre1', '1.1.1.pre2').should == -1 + subject.compare_versions('1.1.1.pre2', '1.1.1.pre1').should == 1 + subject.compare_versions('1.1.1.pre1', '1.1.1.pre2').should == -1 + end end + shared_context 'with released version at 3.9.7' do + before do + Excon.stub({:host => 'assets.heroku.com', :path => '/heroku-client/VERSION'}, {:body => "3.9.7\n"}) + end + end + + shared_context 'with local version at 3.9.6' do + before do + subject.stub(:latest_local_version).and_return('3.9.6') + end + end + + shared_context 'with local version at 3.9.7' do + before do + subject.stub(:latest_local_version).and_return('3.9.7') + end + end + + describe '::update' do + include_context 'with released version at 3.9.7' + + describe 'non-beta' do + before do + zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) + hash = "615792e1f06800a6d744f518887b10c09aa914eab51d0f7fbbefd81a8a64af93" + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/zip'}, {:body => zip}) + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/update/hash'}, {:body => "#{hash}\n"}) + end + + context 'with no update available' do + include_context 'with local version at 3.9.7' + + it 'does not update' do + expect(subject.update(false)).to be_false + end + end + + context 'with an update available' do + include_context 'with local version at 3.9.6' + + it 'updates' do + expect(subject.update(false)).to eq('3.9.7') + end + end + end + + describe 'beta' do + before do + zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) + hash = "615792e1f06800a6d744f518887b10c09aa914eab51d0f7fbbefd81a8a64af93" + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/beta-zip'}, {:body => zip}) + Excon.stub({:host => 'toolbelt.heroku.com', :path => '/update/hash'}, {:body => "#{hash}\n"}) + end + + context 'with no update available' do + include_context 'with local version at 3.9.7' + + it 'still updates' do + expect(subject.update(true)).to eq('3.9.7') + end + end + end + end end end From 95a4207c46d0e5543c8247ebbce4c03645bed77a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 15:19:07 -0700 Subject: [PATCH 014/952] do not check hash for prereleases --- Gemfile.lock | 2 +- lib/heroku/updater.rb | 6 ++++-- spec/heroku/updater_spec.rb | 2 -- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4400f764b..6a900ef58 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.9.7) + heroku (3.9.8.pre) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index d64afe5f4..94563e668 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -102,8 +102,10 @@ def self.update(prerelease) end download_file(url, zip_filename) - hash = Digest::SHA256.file(zip_filename).hexdigest - error "Update hash signature mismatch" unless hash == official_zip_hash + unless prerelease + hash = Digest::SHA256.file(zip_filename).hexdigest + error "Update hash signature mismatch" unless hash == official_zip_hash + end extract_zip(zip_filename, download_dir) FileUtils.rm_f zip_filename diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index 7c256b66e..87c6acbab 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -93,9 +93,7 @@ module Heroku describe 'beta' do before do zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) - hash = "615792e1f06800a6d744f518887b10c09aa914eab51d0f7fbbefd81a8a64af93" Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/beta-zip'}, {:body => zip}) - Excon.stub({:host => 'toolbelt.heroku.com', :path => '/update/hash'}, {:body => "#{hash}\n"}) end context 'with no update available' do From c8490bbda00c6ba6b5112f6d7d8ab94ff4ecde74 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 15:22:32 -0700 Subject: [PATCH 015/952] v3.10.0 --- CHANGELOG | 5 +++-- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index da80a07ce..e7057ae1d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ -3.9.8 2014-08-13 -================ +3.10.0 2014-08-14 +================= +Fixed beta releases Upgrade heroku-api and excon 3.9.7 2014-08-12 diff --git a/Gemfile.lock b/Gemfile.lock index 6a900ef58..902a5e21f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.9.8.pre) + heroku (3.10.0.pre) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 432752276..60ff45c1b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.9.8.pre" + VERSION = "3.10.0.pre" end From 835d611d6656516a5eda456e8db907ff664bfec7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 13:22:23 -0700 Subject: [PATCH 016/952] test on ruby 2.0.0 and 2.1.2 for future compatibility --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 635721991..34ef14d6c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,5 +18,7 @@ rvm: - 1.8.7 - 1.9.2 - 1.9.3 + - 2.0.0 + - 2.1.2 script: bundle exec rspec -bfs spec From dc816716f577f23ccfbfd3272b022d43faf67994 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 12:54:43 -0700 Subject: [PATCH 017/952] added cove coverage via simplecov --- Gemfile | 1 + Gemfile.lock | 17 +++++++++++++++++ README.md | 1 + spec/spec_helper.rb | 3 +++ 4 files changed, 22 insertions(+) diff --git a/Gemfile b/Gemfile index 77d3fd114..ba64de379 100644 --- a/Gemfile +++ b/Gemfile @@ -20,4 +20,5 @@ group :test do gem "rspec", ">= 2.0" gem "sqlite3" gem "webmock" + gem "coveralls", :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index 902a5e21f..19225447a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,8 +23,15 @@ GEM cabin (0.4.4) json clamp (0.5.0) + coveralls (0.7.0) + multi_json (~> 1.3) + rest-client + simplecov (>= 0.7) + term-ansicolor + thor crack (0.3.2) diff-lcs (1.1.3) + docile (1.1.5) excon (0.39.4) fakefs (0.4.2) fpm (0.4.6) @@ -55,8 +62,17 @@ GEM diff-lcs (~> 1.1.3) rspec-mocks (2.12.2) rubyzip (0.9.9) + simplecov (0.9.0) + docile (~> 1.1.0) + multi_json + simplecov-html (~> 0.8.0) + simplecov-html (0.8.0) sqlite3 (1.3.7) sqlite3 (1.3.7-x86-mingw32) + term-ansicolor (1.3.0) + tins (~> 1.0) + thor (0.19.1) + tins (1.3.1) webmock (1.9.0) addressable (>= 2.2.7) crack (>= 0.1.7) @@ -68,6 +84,7 @@ PLATFORMS DEPENDENCIES aws-s3 + coveralls fakefs fpm heroku! diff --git a/README.md b/README.md index 69aadedd3..0c29dfabd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ For more about Heroku see . To get started see [![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) +[![Coverage Status](https://img.shields.io/coveralls/heroku/heroku.svg)](https://coveralls.io/r/heroku/heroku?branch=master) [![Dependency Status](https://gemnasium.com/heroku/heroku.svg)](https://gemnasium.com/heroku/heroku) Setup diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b3bfda110..bcb4de926 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,6 +2,9 @@ require "rubygems" +require "coveralls" +Coveralls.wear! + require "excon" # ensure these are around for errors From 93ddc28ee61f3c31e8399fcc21f3dd7904256778 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 16:08:44 -0700 Subject: [PATCH 018/952] v3.10.0 --- lib/heroku/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 60ff45c1b..b997c7c01 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.0.pre" + VERSION = "3.10.0" end From df8c2f810adb4ddfedf3992010629177ea46d6fc Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 16:09:13 -0700 Subject: [PATCH 019/952] v3.10.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 19225447a..00cbbcdd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.0.pre) + heroku (3.10.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) From 7a22f62c449e15d6788c6577c7233f2af2ea24a0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 16:15:04 -0700 Subject: [PATCH 020/952] v3.10.1 --- CHANGELOG | 4 ++++ lib/heroku/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index e7057ae1d..a045886b4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.10.1 2014-08-14 +================= +No changes, just verifying new release code is in order + 3.10.0 2014-08-14 ================= Fixed beta releases diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index b997c7c01..9278039e3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.0" + VERSION = "3.10.1" end From 8aadb36176e0852fffccb1d8b24787b70c0b8db7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Aug 2014 16:18:08 -0700 Subject: [PATCH 021/952] v3.10.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 00cbbcdd8..d0b55f1ea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.0) + heroku (3.10.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) From 26a73e3a5a62f3958ea6f6b64925011341382033 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 10:35:25 -0700 Subject: [PATCH 022/952] tests passing --- Gemfile | 2 +- Gemfile.lock | 20 +++--- spec/helper/pg_dump_restore_spec.rb | 18 +++--- spec/heroku/auth_spec.rb | 56 ++++++++--------- spec/heroku/client/heroku_postgresql_spec.rb | 4 +- spec/heroku/client_spec.rb | 38 ++++++------ spec/heroku/command/addons_spec.rb | 62 +++++++++---------- spec/heroku/command/base_spec.rb | 28 ++++----- spec/heroku/command/pgbackups_spec.rb | 44 +++++++------ spec/heroku/helpers/heroku_postgresql_spec.rb | 6 +- spec/heroku/plugin_spec.rb | 26 ++++---- spec/heroku/updater_spec.rb | 4 +- spec/spec_helper.rb | 13 ++-- spec/support/openssl_mock_helper.rb | 6 +- 14 files changed, 162 insertions(+), 165 deletions(-) diff --git a/Gemfile b/Gemfile index ba64de379..6f94b6cf0 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ group :test do gem "fakefs" gem "jruby-openssl", :platform => :jruby gem "json" - gem "rspec", ">= 2.0" + gem "rspec", '~> 2.99' gem "sqlite3" gem "webmock" gem "coveralls", :require => false diff --git a/Gemfile.lock b/Gemfile.lock index d0b55f1ea..c30bad5db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,7 +30,7 @@ GEM term-ansicolor thor crack (0.3.2) - diff-lcs (1.1.3) + diff-lcs (1.2.5) docile (1.1.5) excon (0.39.4) fakefs (0.4.2) @@ -53,14 +53,14 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rr (1.0.4) - rspec (2.12.0) - rspec-core (~> 2.12.0) - rspec-expectations (~> 2.12.0) - rspec-mocks (~> 2.12.0) - rspec-core (2.12.2) - rspec-expectations (2.12.1) - diff-lcs (~> 1.1.3) - rspec-mocks (2.12.2) + rspec (2.99.0) + rspec-core (~> 2.99.0) + rspec-expectations (~> 2.99.0) + rspec-mocks (~> 2.99.0) + rspec-core (2.99.1) + rspec-expectations (2.99.2) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.99.2) rubyzip (0.9.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -92,7 +92,7 @@ DEPENDENCIES json rake (>= 0.8.7) rr (~> 1.0.2) - rspec (>= 2.0) + rspec (~> 2.99) rubyzip sqlite3 webmock diff --git a/spec/helper/pg_dump_restore_spec.rb b/spec/helper/pg_dump_restore_spec.rb index f99020783..5e8ed5c9b 100644 --- a/spec/helper/pg_dump_restore_spec.rb +++ b/spec/helper/pg_dump_restore_spec.rb @@ -7,18 +7,18 @@ end it 'requires uris for from and to arguments' do - expect { PgDumpRestore.new(nil , @localdb, mock) }.to raise_error - expect { PgDumpRestore.new(@remotedb, nil , mock) }.to raise_error - expect { PgDumpRestore.new(@remotedb, @localdb, mock) }.to_not raise_error + expect { PgDumpRestore.new(nil , @localdb, double) }.to raise_error + expect { PgDumpRestore.new(@remotedb, nil , double) }.to raise_error + expect { PgDumpRestore.new(@remotedb, @localdb, double) }.to_not raise_error end it 'uses PGPORT from ENV to set local port' do ENV['PGPORT'] = '15432' - expect(PgDumpRestore.new(@remotedb, @localdb, mock).instance_variable_get('@target').port).to eq 15432 + expect(PgDumpRestore.new(@remotedb, @localdb, double).instance_variable_get('@target').port).to eq 15432 end it 'on pulls, prepare requires the local database to not exist' do - mock_command = mock + mock_command = double mock_command.should_receive(:error).once pgdr = PgDumpRestore.new(@remotedb, @localdb, mock_command) pgdr.should_receive(:`).once.and_return(`false`) @@ -27,7 +27,7 @@ end it 'on pushes, prepare requires the remote database to be empty' do - mock_command = mock + mock_command = double mock_command.should_receive(:error).once pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) mock_command.should_receive(:exec_sql_on_uri).once.and_return("something that isn't a true") @@ -35,7 +35,7 @@ end it 'executes a proper dump/restore command' do - pgdr = PgDumpRestore.new(@remotedb, @localdb, mock) + pgdr = PgDumpRestore.new(@remotedb, @localdb, double) expect(pgdr.dump_restore_cmd).to match(/ pg_dump .* remotehost .* @@ -49,7 +49,7 @@ describe 'verification' do it 'errors when the extensions do not match' do - mock_command = mock + mock_command = double mock_command.should_receive(:error).once pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) mock_command.should_receive(:exec_sql_on_uri).twice.and_return("these", "don't match") @@ -57,7 +57,7 @@ end it 'is fine when the extensions match' do - mock_command = mock + mock_command = double mock_command.should_not_receive(:error) pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) mock_command.should_receive(:exec_sql_on_uri).twice.and_return("these match", "these match") diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index ee9a04da9..6bb48b3e7 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -10,16 +10,16 @@ module Heroku ENV['HEROKU_API_KEY'] = nil @cli = Heroku::Auth - @cli.stub!(:check) - @cli.stub!(:display) - @cli.stub!(:running_on_a_mac?).and_return(false) + @cli.stub(:check) + @cli.stub(:display) + @cli.stub(:running_on_a_mac?).and_return(false) @cli.credentials = nil FakeFS.activate! - FakeFS::File.stub!(:stat).and_return(double('stat', :mode => "0600".to_i(8))) - FakeFS::FileUtils.stub!(:chmod) - FakeFS::File.stub!(:readlines) do |path| + FakeFS::File.stub(:stat).and_return(double('stat', :mode => "0600".to_i(8))) + FakeFS::FileUtils.stub(:chmod) + FakeFS::File.stub(:readlines) do |path| File.read(path).split("\n").map {|line| "#{line}\n"} end @@ -85,8 +85,8 @@ module Heroku context "reauthenticating" do before do - @cli.stub!(:ask_for_credentials).and_return(['new_user', 'new_password']) - @cli.stub!(:check) + @cli.stub(:ask_for_credentials).and_return(['new_user', 'new_password']) + @cli.stub(:check) @cli.should_receive(:check_for_associated_ssh_key) @cli.reauthorize end @@ -103,7 +103,7 @@ module Heroku @cli.logout end it "should delete saved credentials" do - File.exists?(@cli.legacy_credentials_path).should be_false + File.exists?(@cli.legacy_credentials_path).should be_falsey Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should be_nil end end @@ -128,29 +128,29 @@ module Heroku end it "writes credentials and uploads authkey when credentials are saved" do - @cli.stub!(:credentials) - @cli.stub!(:check) - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") + @cli.stub(:credentials) + @cli.stub(:check) + @cli.stub(:ask_for_credentials).and_return("username", "apikey") @cli.should_receive(:write_credentials) @cli.should_receive(:check_for_associated_ssh_key) @cli.ask_for_and_save_credentials end it "save_credentials deletes the credentials when the upload authkey is unauthorized" do - @cli.stub!(:write_credentials) - @cli.stub!(:retry_login?).and_return(false) - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") - @cli.stub!(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } + @cli.stub(:write_credentials) + @cli.stub(:retry_login?).and_return(false) + @cli.stub(:ask_for_credentials).and_return("username", "apikey") + @cli.stub(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } @cli.should_receive(:delete_credentials) lambda { @cli.ask_for_and_save_credentials }.should raise_error(SystemExit) end it "asks for login again when not authorized, for three times" do - @cli.stub!(:read_credentials) - @cli.stub!(:write_credentials) - @cli.stub!(:delete_credentials) - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") - @cli.stub!(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } + @cli.stub(:read_credentials) + @cli.stub(:write_credentials) + @cli.stub(:delete_credentials) + @cli.stub(:ask_for_credentials).and_return("username", "apikey") + @cli.stub(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } @cli.should_receive(:ask_for_credentials).exactly(3).times lambda { @cli.ask_for_and_save_credentials }.should raise_error(SystemExit) end @@ -165,8 +165,8 @@ module Heroku end it "writes the login information to the credentials file for the 'heroku login' command" do - @cli.stub!(:ask_for_credentials).and_return(['one', 'two']) - @cli.stub!(:check) + @cli.stub(:ask_for_credentials).and_return(['one', 'two']) + @cli.stub(:check) @cli.should_receive(:check_for_associated_ssh_key) @cli.reauthorize Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == (['one', 'two']) @@ -186,13 +186,13 @@ module Heroku describe "automatic key uploading" do before(:each) do FileUtils.mkdir_p("#{@cli.home_directory}/.ssh") - @cli.stub!(:ask_for_credentials).and_return("username", "apikey") + @cli.stub(:ask_for_credentials).and_return("username", "apikey") end describe "an account with existing keys" do before :each do - @api = mock(Object) - @response = mock(Object) + @api = double(Object) + @response = double(Object) @response.should_receive(:body).and_return(['existingkeys']) @api.should_receive(:get_keys).and_return(@response) @cli.should_receive(:api).and_return(@api) @@ -206,8 +206,8 @@ module Heroku describe "an account with no keys" do before :each do - @api = mock(Object) - @response = mock(Object) + @api = double(Object) + @response = double(Object) @response.should_receive(:body).and_return([]) @api.should_receive(:get_keys).and_return(@response) @cli.should_receive(:api).and_return(@api) diff --git a/spec/heroku/client/heroku_postgresql_spec.rb b/spec/heroku/client/heroku_postgresql_spec.rb index 840e24a79..7e13e4754 100644 --- a/spec/heroku/client/heroku_postgresql_spec.rb +++ b/spec/heroku/client/heroku_postgresql_spec.rb @@ -14,7 +14,7 @@ describe 'api choosing' do it "sends an ingress request to the client for production plans" do - attachment.stub! :starter_plan? => false + attachment.stub :starter_plan? => false host = 'postgres-api.heroku.com' url = "https://user@example.com:apitoken@#{host}/client/v11/databases/#{attachment.resource_name}/ingress" @@ -29,7 +29,7 @@ end it "sends an ingress request to the client for production plans" do - attachment.stub! :starter_plan? => true + attachment.stub :starter_plan? => true host = 'postgres-starter-api.heroku.com' url = "https://user@example.com:apitoken@#{host}/client/v11/databases/#{attachment.resource_name}/ingress" diff --git a/spec/heroku/client_spec.rb b/spec/heroku/client_spec.rb index 56c2cceac..2af5c5342 100644 --- a/spec/heroku/client_spec.rb +++ b/spec/heroku/client_spec.rb @@ -8,8 +8,8 @@ before do @client = Heroku::Client.new(nil, nil) - @resource = mock('heroku rest resource') - @client.stub!(:extract_warning) + @resource = double('heroku rest resource') + @client.stub(:extract_warning) end it "Client.auth -> get user details" do @@ -49,8 +49,8 @@ EOXML - @client.stub!(:list_collaborators).and_return([:jon, :mike]) - @client.stub!(:installed_addons).and_return([:addon1]) + @client.stub(:list_collaborators).and_return([:jon, :mike]) + @client.stub(:installed_addons).and_return([:addon1]) capture_stderr do # capture deprecation message @client.info('example').should == { :blessed => 'true', :created_at => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'example', :production => 'true', :share_public => 'true', :domain_name => nil, :collaborators => [:jon, :mike], :addons => [:addon1] } end @@ -77,12 +77,12 @@ end it "create_complete?(name) -> checks if a create request is complete" do - @response = mock('response') + @response = double('response') @response.should_receive(:code).and_return(202) @client.should_receive(:resource).and_return(@resource) @resource.should_receive(:put).with({}, @client.heroku_headers).and_return(@response) capture_stderr do # capture deprecation message - @client.create_complete?('example').should be_false + @client.create_complete?('example').should be_falsey end end @@ -204,8 +204,8 @@ it "rake catches 502s and shows the app crashlog" do e = RestClient::RequestFailed.new - e.stub!(:http_code).and_return(502) - e.stub!(:http_body).and_return('the crashlog') + e.stub(:http_code).and_return(502) + e.stub(:http_body).and_return('the crashlog') @client.should_receive(:post).and_raise(e) capture_stderr do # capture deprecation message lambda { @client.rake('example', '') }.should raise_error(Heroku::Client::AppCrashed) @@ -214,8 +214,8 @@ it "rake passes other status codes (i.e., 500) as standard restclient exceptions" do e = RestClient::RequestFailed.new - e.stub!(:http_code).and_return(500) - e.stub!(:http_body).and_return('not a crashlog') + e.stub(:http_code).and_return(500) + e.stub(:http_body).and_return('not a crashlog') @client.should_receive(:post).and_raise(e) capture_stderr do # capture deprecation message lambda { @client.rake('example', '') }.should raise_error(RestClient::RequestFailed) @@ -439,14 +439,14 @@ stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) - @client.uninstall_addon('example', 'addon1').should be_true + @client.uninstall_addon('example', 'addon1').should be_truthy end it "uninstall_addon(app_name, addon_name) with confirmation" do stub_api_request(:delete, "/apps/example/addons/addon1?confirm=example"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) - @client.uninstall_addon('example', 'addon1', :confirm => "example").should be_true + @client.uninstall_addon('example', 'addon1', :confirm => "example").should be_truthy end it "install_addon(app_name, addon_name) with response" do @@ -488,9 +488,9 @@ end it "creates a RestClient resource for making calls" do - @client.stub!(:host).and_return('heroku.com') - @client.stub!(:user).and_return('joe@example.com') - @client.stub!(:password).and_return('secret') + @client.stub(:host).and_return('heroku.com') + @client.stub(:user).and_return('joe@example.com') + @client.stub(:password).and_return('secret') res = @client.resource('/xyz') @@ -511,7 +511,7 @@ end it "runs a callback when the API sets a warning header" do - response = mock('rest client response', :headers => { :x_heroku_warning => 'Warning' }) + response = double('rest client response', :headers => { :x_heroku_warning => 'Warning' }) @client.should_receive(:resource).and_return(@resource) @resource.should_receive(:get).and_return(response) @client.on_warning { |msg| @callback = msg } @@ -520,9 +520,9 @@ end it "doesn't run the callback twice for the same warning" do - response = mock('rest client response', :headers => { :x_heroku_warning => 'Warning' }) - @client.stub!(:resource).and_return(@resource) - @resource.stub!(:get).and_return(response) + response = double('rest client response', :headers => { :x_heroku_warning => 'Warning' }) + @client.stub(:resource).and_return(@resource) + @resource.stub(:get).and_return(response) @client.on_warning { |msg| @callback_called ||= 0; @callback_called += 1 } @client.get('test1') @client.get('test2') diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 56dddb35f..a29941e00 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -96,7 +96,7 @@ module Heroku::Command describe 'v1-style command line params' do it "understands foo=baz" do - @addons.stub!(:args).and_return(%w(my_addon foo=baz)) + @addons.stub(:args).and_return(%w(my_addon foo=baz)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.add end @@ -129,31 +129,31 @@ module Heroku::Command describe 'unix-style command line params' do it "understands --foo=baz" do - @addons.stub!(:args).and_return(%w(my_addon --foo=baz)) + @addons.stub(:args).and_return(%w(my_addon --foo=baz)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.add end it "understands --foo baz" do - @addons.stub!(:args).and_return(%w(my_addon --foo baz)) + @addons.stub(:args).and_return(%w(my_addon --foo baz)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.add end it "treats lone switches as true" do - @addons.stub!(:args).and_return(%w(my_addon --foo)) + @addons.stub(:args).and_return(%w(my_addon --foo)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) @addons.add end it "converts 'true' to boolean" do - @addons.stub!(:args).and_return(%w(my_addon --foo=true)) + @addons.stub(:args).and_return(%w(my_addon --foo=true)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) @addons.add end it "works with many config vars" do - @addons.stub!(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) + @addons.stub(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => true, 'bob' => true }) @addons.add end @@ -166,14 +166,14 @@ module Heroku::Command end it "raises an error for spurious arguments" do - @addons.stub!(:args).and_return(%w(my_addon spurious)) + @addons.stub(:args).and_return(%w(my_addon spurious)) lambda { @addons.add }.should raise_error(CommandFailed) end end describe "mixed options" do it "understands foo=bar and --baz=bar on the same line" do - @addons.stub!(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) + @addons.stub(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'baz' => 'bar', 'bar' => true, 'bob' => true }) @addons.add end @@ -190,7 +190,7 @@ module Heroku::Command describe "fork, follow, and rollback switches" do it "should only resolve for heroku-postgresql addon" do %w{fork follow rollback}.each do |switch| - @addons.stub!(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) + @addons.stub(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) @addons.heroku.should_receive(:install_addon). with('example', 'addon', {switch => 'HEROKU_POSTGRESQL_RED'}) @addons.add @@ -208,7 +208,7 @@ module Heroku::Command 'value' => 'postgres://red_url', 'type' => 'heroku-postgresql:ronin' }}) ]) - @addons.stub!(:args).and_return("heroku-postgresql --#{switch} HEROKU_POSTGRESQL_RED".split) + @addons.stub(:args).and_return("heroku-postgresql --#{switch} HEROKU_POSTGRESQL_RED".split) @addons.heroku.should_receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://red_url'}) @addons.add end @@ -216,9 +216,9 @@ module Heroku::Command it "should NOT translate --fork and --follow if passed in a full postgres url even if there are no databases" do %w{fork follow}.each do |switch| - @addons.stub!(:app_config_vars).and_return({}) - @addons.stub!(:app_attachments).and_return([]) - @addons.stub!(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) + @addons.stub(:app_config_vars).and_return({}) + @addons.stub(:app_attachments).and_return([]) + @addons.stub(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) @addons.heroku.should_receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://foo:yeah@awesome.com:234/bestdb'}) @addons.add end @@ -226,9 +226,9 @@ module Heroku::Command it "should fail if fork / follow across applications and no plan is specified" do %w{fork follow}.each do |switch| - @addons.stub!(:app_config_vars).and_return({}) - @addons.stub!(:app_attachments).and_return([]) - @addons.stub!(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) + @addons.stub(:app_config_vars).and_return({}) + @addons.stub(:app_attachments).and_return([]) + @addons.stub(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) lambda { @addons.add }.should raise_error(CommandFailed) end end @@ -236,7 +236,7 @@ module Heroku::Command describe 'adding' do before do - @addons.stub!(:args).and_return(%w(my_addon)) + @addons.stub(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -255,12 +255,12 @@ module Heroku::Command it "requires an addon name" do - @addons.stub!(:args).and_return([]) + @addons.stub(:args).and_return([]) lambda { @addons.add }.should raise_error(CommandFailed) end it "adds an addon" do - @addons.stub!(:args).and_return(%w(my_addon)) + @addons.stub(:args).and_return(%w(my_addon)) @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', {}) @addons.add end @@ -314,7 +314,7 @@ module Heroku::Command describe 'upgrading' do before do - @addons.stub!(:args).and_return(%w(my_addon)) + @addons.stub(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -332,18 +332,18 @@ module Heroku::Command end it "requires an addon name" do - @addons.stub!(:args).and_return([]) + @addons.stub(:args).and_return([]) lambda { @addons.upgrade }.should raise_error(CommandFailed) end it "upgrades an addon" do - @addons.stub!(:args).and_return(%w(my_addon)) + @addons.stub(:args).and_return(%w(my_addon)) @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', {}) @addons.upgrade end it "upgrade an addon with config vars" do - @addons.stub!(:args).and_return(%w(my_addon --foo=baz)) + @addons.stub(:args).and_return(%w(my_addon --foo=baz)) @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.upgrade end @@ -372,7 +372,7 @@ module Heroku::Command describe 'downgrading' do before do - @addons.stub!(:args).and_return(%w(my_addon)) + @addons.stub(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -390,18 +390,18 @@ module Heroku::Command end it "requires an addon name" do - @addons.stub!(:args).and_return([]) + @addons.stub(:args).and_return([]) lambda { @addons.downgrade }.should raise_error(CommandFailed) end it "downgrades an addon" do - @addons.stub!(:args).and_return(%w(my_addon)) + @addons.stub(:args).and_return(%w(my_addon)) @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', {}) @addons.downgrade end it "downgrade an addon with config vars" do - @addons.stub!(:args).and_return(%w(my_addon --foo=baz)) + @addons.stub(:args).and_return(%w(my_addon --foo=baz)) @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.downgrade end @@ -429,22 +429,22 @@ module Heroku::Command end it "does not remove addons with no confirm" do - @addons.stub!(:args).and_return(%w( addon1 )) + @addons.stub(:args).and_return(%w( addon1 )) @addons.should_receive(:confirm_command).once.and_return(false) @addons.heroku.should_not_receive(:uninstall_addon) @addons.remove end it "removes addons after prompting for confirmation" do - @addons.stub!(:args).and_return(%w( addon1 )) + @addons.stub(:args).and_return(%w( addon1 )) @addons.should_receive(:confirm_command).once.and_return(true) @addons.heroku.should_receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") @addons.remove end it "removes addons with confirm option" do - Heroku::Command.stub!(:current_options).and_return(:confirm => "example") - @addons.stub!(:args).and_return(%w( addon1 )) + Heroku::Command.stub(:current_options).and_return(:confirm => "example") + @addons.stub(:args).and_return(%w( addon1 )) @addons.heroku.should_receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") @addons.remove end diff --git a/spec/heroku/command/base_spec.rb b/spec/heroku/command/base_spec.rb index acbd3af94..1fc7f31c3 100644 --- a/spec/heroku/command/base_spec.rb +++ b/spec/heroku/command/base_spec.rb @@ -5,15 +5,15 @@ module Heroku::Command describe Base do before do @base = Base.new - @base.stub!(:display) - @client = mock('heroku client', :host => 'heroku.com') + @base.stub(:display) + @client = double('heroku client', :host => 'heroku.com') end describe "confirming" do it "confirms the app via --confirm" do Heroku::Command.stub(:current_options).and_return(:confirm => "example") @base.stub(:app).and_return("example") - @base.confirm_command.should be_true + @base.confirm_command.should be_truthy end it "does not confirms the app via --confirm on a mismatch" do @@ -26,7 +26,7 @@ module Heroku::Command @base.stub(:app).and_return("example") @base.stub(:ask).and_return("example") Heroku::Command.stub(:current_options).and_return({}) - @base.confirm_command.should be_true + @base.confirm_command.should be_truthy end it "fails if the interactive confirm doesn't match" do @@ -43,26 +43,26 @@ module Heroku::Command context "detecting the app" do it "attempts to find the app via the --app option" do - @base.stub!(:options).and_return(:app => "example") + @base.stub(:options).and_return(:app => "example") @base.app.should == "example" end it "attempts to find the app via the --confirm option" do - @base.stub!(:options).and_return(:confirm => "myconfirmapp") + @base.stub(:options).and_return(:confirm => "myconfirmapp") @base.app.should == "myconfirmapp" end it "attempts to find the app via HEROKU_APP when not explicitly specified" do ENV['HEROKU_APP'] = "myenvapp" @base.app.should == "myenvapp" - @base.stub!(:options).and_return([]) + @base.stub(:options).and_return([]) @base.app.should == "myenvapp" ENV.delete('HEROKU_APP') end it "overrides HEROKU_APP when explicitly specified" do ENV['HEROKU_APP'] = "myenvapp" - @base.stub!(:options).and_return(:app => "example") + @base.stub(:options).and_return(:app => "example") @base.app.should == "example" ENV.delete('HEROKU_APP') end @@ -79,7 +79,7 @@ module Heroku::Command other\tgit@other.com:other.git (push) REMOTES - @heroku = mock + @heroku = double @heroku.stub(:host).and_return('heroku.com') @base.stub(:heroku).and_return(@heroku) @@ -88,19 +88,19 @@ module Heroku::Command end it "gets the app from remotes when there's only one app" do - @base.stub!(:git_remotes).and_return({ 'heroku' => 'example' }) - @base.stub!(:git).with("config heroku.remote").and_return("") + @base.stub(:git_remotes).and_return({ 'heroku' => 'example' }) + @base.stub(:git).with("config heroku.remote").and_return("") @base.app.should == 'example' end it "accepts a --remote argument to choose the app from the remote name" do - @base.stub!(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) - @base.stub!(:options).and_return(:remote => "staging") + @base.stub(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) + @base.stub(:options).and_return(:remote => "staging") @base.app.should == 'example-staging' end it "raises when cannot determine which app is it" do - @base.stub!(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) + @base.stub(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) lambda { @base.app }.should raise_error(Heroku::Command::CommandFailed) end end diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index ed44be4ea..b07e2504f 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -18,7 +18,7 @@ module Heroku::Command describe Pgbackups do before do @pgbackups = prepare_command(Pgbackups) - @pgbackups.heroku.stub!(:info).and_return({}) + @pgbackups.heroku.stub(:info).and_return({}) api.post_app("name" => "example") api.put_config_vars( @@ -77,7 +77,7 @@ module Heroku::Command let(:from_url) { "postgres://from/bar" } let(:attachment) { double('attachment', :display_name => from_name, :url => from_url ) } before do - @pgbackups.stub!(:resolve).and_return(attachment) + @pgbackups.stub(:resolve).and_return(attachment) end it "gets the url for the latest backup if nothing is specified" do @@ -85,34 +85,34 @@ module Heroku::Command stub_pgbackups.get_latest_backup.returns({"public_url" => "http://latest/backup.dump"}) old_stdout_isatty = STDOUT.isatty - $stdout.stub!(:isatty).and_return(true) + $stdout.stub(:isatty).and_return(true) stderr, stdout = execute("pgbackups:url") stderr.should == "" stdout.should == <<-STDOUT http://latest/backup.dump STDOUT - $stdout.stub!(:isatty).and_return(old_stdout_isatty) + $stdout.stub(:isatty).and_return(old_stdout_isatty) end it "gets the url for the named backup if a name is specified" do stub_pgbackups.get_backup.with("b001").returns({"public_url" => "http://latest/backup.dump" }) old_stdout_isatty = STDOUT.isatty - $stdout.stub!(:isatty).and_return(true) + $stdout.stub(:isatty).and_return(true) stderr, stdout = execute("pgbackups:url b001") stderr.should == "" stdout.should == <<-STDOUT http://latest/backup.dump STDOUT - $stdout.stub!(:isatty).and_return(old_stdout_isatty) + $stdout.stub(:isatty).and_return(old_stdout_isatty) end it "should capture a backup when requested" do backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - @pgbackups.stub!(:args).and_return([]) - @pgbackups.stub!(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) - @pgbackups.stub!(:poll_transfer!).with(backup_obj).and_return(backup_obj) + @pgbackups.stub(:args).and_return([]) + @pgbackups.stub(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) + @pgbackups.stub(:poll_transfer!).with(backup_obj).and_return(backup_obj) @pgbackups.capture end @@ -120,9 +120,9 @@ module Heroku::Command it "should send expiration flag to client if specified on args" do backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - @pgbackups.stub!(:options).and_return({:expire => true}) - @pgbackups.stub!(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) - @pgbackups.stub!(:poll_transfer!).with(backup_obj).and_return(backup_obj) + @pgbackups.stub(:options).and_return({:expire => true}) + @pgbackups.stub(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) + @pgbackups.stub(:poll_transfer!).with(backup_obj).and_return(backup_obj) @pgbackups.capture end @@ -221,14 +221,12 @@ def stub_failed_capture(log) context "restore" do let(:attachment) { double('attachment', :display_name => 'someconfigvar', :url => 'postgres://fromhost/database') } before do - from_name, from_url = "FROM_NAME", "postgres://fromhost/database" - - @pgbackups_client = RSpec::Mocks::Mock.new("pgbackups_client") # avoid double r mock - @pgbackups.stub!(:pgbackup_client).and_return(@pgbackups_client) + @pgbackups_client = double("pgbackups_client") + @pgbackups.stub(:pgbackup_client).and_return(@pgbackups_client) end it "should receive a confirm_command on restore" do - @pgbackups_client.stub!(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } + @pgbackups_client.stub(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } @pgbackups.should_receive(:confirm_command).and_return(false) @pgbackups_client.should_not_receive(:transfer!) @@ -250,9 +248,9 @@ def stub_failed_capture(log) "from_name" => "postgres://databasehost/dbname" } - @pgbackups.stub!(:confirm_command).and_return(true) + @pgbackups.stub(:confirm_command).and_return(true) @pgbackups_client.should_receive(:create_transfer).and_return(@backup_obj) - @pgbackups.stub!(:poll_transfer!).and_return(@backup_obj) + @pgbackups.stub(:poll_transfer!).and_return(@backup_obj) end it "should default to the latest backup" do @@ -285,8 +283,8 @@ def stub_failed_capture(log) context "on errors" do before(:each) do - @pgbackups_client.stub!(:get_latest_backup => {"to_url" => "s3://bucket/user/bXXX.dump"} ) - @pgbackups.stub!(:confirm_command => true) + @pgbackups_client.stub(:get_latest_backup => {"to_url" => "s3://bucket/user/bXXX.dump"} ) + @pgbackups.stub(:confirm_command => true) end def stub_error_backup_with_log(log) @@ -296,7 +294,7 @@ def stub_error_backup_with_log(log) } @pgbackups_client.should_receive(:create_transfer) { @backup_obj } - @pgbackups.stub!(:poll_transfer!) { @backup_obj } + @pgbackups.stub(:poll_transfer!) { @backup_obj } end it 'aborts for a generic error' do @@ -307,7 +305,7 @@ def stub_error_backup_with_log(log) it 'aborts and informs for expired s3 urls' do stub_error_backup_with_log 'Invalid dump format: /tmp/aDMyoXPrAX/b031.dump: XML document text' - @pgbackups.should_receive(:error).with { |message| message.should =~ /backup url is invalid/ } + @pgbackups.should_receive(:error).with(/backup url is invalid/) @pgbackups.restore end end diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index 5f0d285a9..4d5d26325 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -6,7 +6,7 @@ describe Heroku::Helpers::HerokuPostgresql::Resolver do before do - @resolver = described_class.new('appname', mock(:api)) + @resolver = described_class.new('appname', double(:api)) @resolver.stub(:app_config_vars) { app_config_vars } @resolver.stub(:app_attachments) { app_attachments } end @@ -54,11 +54,11 @@ context "when no app is specified or inferred, and identifier does not have app::db shorthand" do it 'exits, complaining about the missing app' do - api = mock('api') + api = double('api') api.stub(:get_attachments).and_raise("getting this far will cause an inaccurate 'internal server error' message") no_app_resolver = described_class.new(nil, api) - no_app_resolver.should_receive(:error).with { |msg| expect(msg).to match(/No app specified/) }.and_raise(SystemExit) + no_app_resolver.should_receive(:error).with(/No app specified/).and_raise(SystemExit) expect { no_app_resolver.resolve('black') }.to raise_error(SystemExit) end end diff --git a/spec/heroku/plugin_spec.rb b/spec/heroku/plugin_spec.rb index f6c650162..33afda590 100644 --- a/spec/heroku/plugin_spec.rb +++ b/spec/heroku/plugin_spec.rb @@ -6,7 +6,7 @@ module Heroku include SandboxHelper it "lives in ~/.heroku/plugins" do - Plugin.stub!(:home_directory).and_return('/home/user') + Plugin.stub(:home_directory).and_return('/home/user') Plugin.directory.should == '/home/user/.heroku/plugins' end @@ -18,8 +18,8 @@ module Heroku before(:each) do @sandbox = "/tmp/heroku_plugins_spec_#{Process.pid}" FileUtils.mkdir_p(@sandbox) - Dir.stub!(:pwd).and_return(@sandbox) - Plugin.stub!(:directory).and_return(@sandbox) + Dir.stub(:pwd).and_return(@sandbox) + Plugin.stub(:directory).and_return(@sandbox) end after(:each) do @@ -38,7 +38,7 @@ module Heroku FileUtils.mkdir_p(plugin_folder) `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_true + File.directory?("#{@sandbox}/heroku_plugin").should be_truthy File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" end @@ -48,7 +48,7 @@ module Heroku `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` Plugin.new(plugin_folder).install Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_true + File.directory?("#{@sandbox}/heroku_plugin").should be_truthy File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" end @@ -64,7 +64,7 @@ module Heroku it "updates existing copies" do Plugin.new('heroku_plugin').update - File.directory?("#{@sandbox}/heroku_plugin").should be_true + File.directory?("#{@sandbox}/heroku_plugin").should be_truthy File.read("#{@sandbox}/heroku_plugin/README").should == "updated\n" end @@ -100,14 +100,14 @@ module Heroku FileUtils.mkdir_p(@sandbox + '/plugin/lib') File.open(@sandbox + '/plugin/lib/my_custom_plugin_file.rb', 'w') { |f| f.write "" } Plugin.load! - lambda { require 'my_custom_plugin_file' }.should_not raise_error(LoadError) + expect { require 'my_custom_plugin_file' }.not_to raise_error end it "loads init.rb, if present" do FileUtils.mkdir_p(@sandbox + '/plugin') File.open(@sandbox + '/plugin/init.rb', 'w') { |f| f.write "LoadedInit = true" } Plugin.load! - LoadedInit.should be_true + LoadedInit.should be_truthy end describe "when there are plugin load errors" do @@ -136,7 +136,7 @@ module Heroku Plugin.load! end stderr.should include('some_non_existant_file (LoadError)') - LoadedPlugin2.should be_true + LoadedPlugin2.should be_truthy end end @@ -151,20 +151,20 @@ module Heroku it "should show confirmation to remove deprecated plugins if in an interactive shell" do old_stdin_isatty = STDIN.isatty - STDIN.stub!(:isatty).and_return(true) + STDIN.stub(:isatty).and_return(true) Plugin.should_receive(:confirm).with("The plugin heroku-releases has been deprecated. Would you like to remove it? (y/N)").and_return(true) Plugin.should_receive(:remove_plugin).with("heroku-releases") Plugin.load! - STDIN.stub!(:isatty).and_return(old_stdin_isatty) + STDIN.stub(:isatty).and_return(old_stdin_isatty) end it "should not prompt for deprecation if not in an interactive shell" do old_stdin_isatty = STDIN.isatty - STDIN.stub!(:isatty).and_return(false) + STDIN.stub(:isatty).and_return(false) Plugin.should_not_receive(:confirm) Plugin.should_not_receive(:remove_plugin).with("heroku-releases") Plugin.load! - STDIN.stub!(:isatty).and_return(old_stdin_isatty) + STDIN.stub(:isatty).and_return(old_stdin_isatty) end end end diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index 87c6acbab..68069b2ba 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -7,7 +7,7 @@ module Heroku describe('::latest_local_version') do it 'calculates the latest local version' do - subject.latest_local_version.should == Heroku::VERSION + expect(subject.latest_local_version).to eq(Heroku::VERSION) end end @@ -77,7 +77,7 @@ module Heroku include_context 'with local version at 3.9.7' it 'does not update' do - expect(subject.update(false)).to be_false + expect(subject.update(false)).to be_nil end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bcb4de926..c8d4a4a2d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -38,12 +38,12 @@ def stub_api_request(method, path) def prepare_command(klass) command = klass.new - command.stub!(:app).and_return("example") - command.stub!(:ask).and_return("") - command.stub!(:display) - command.stub!(:hputs) - command.stub!(:hprint) - command.stub!(:heroku).and_return(mock('heroku client', :host => 'heroku.com')) + command.stub(:app).and_return("example") + command.stub(:ask).and_return("") + command.stub(:display) + command.stub(:hputs) + command.stub(:hprint) + command.stub(:heroku).and_return(double('heroku client', :host => 'heroku.com')) command end @@ -214,7 +214,6 @@ def home_directory require "support/organizations_mock_helper" RSpec.configure do |config| - config.color_enabled = true config.include DisplayMessageMatcher config.order = 'rand' config.before { Heroku::Helpers.error_with_failure = false } diff --git a/spec/support/openssl_mock_helper.rb b/spec/support/openssl_mock_helper.rb index 70268c482..0bf9621f8 100644 --- a/spec/support/openssl_mock_helper.rb +++ b/spec/support/openssl_mock_helper.rb @@ -1,7 +1,7 @@ def mock_openssl - @ctx_mock = mock "SSLContext", :key= => nil, :cert= => nil, :ssl_version= => nil - @tcp_socket_mock = mock "TCPSocket", :close => true - @ssl_socket_mock = mock "SSLSocket", :sync= => true, :connect => true, :close => true, :to_io => $stdin + @ctx_mock = double "SSLContext", :key= => nil, :cert= => nil, :ssl_version= => nil + @tcp_socket_mock = double "TCPSocket", :close => true + @ssl_socket_mock = double "SSLSocket", :sync= => true, :connect => true, :close => true, :to_io => $stdin OpenSSL::SSL::SSLSocket.stub(:new).and_return(@ssl_socket_mock) OpenSSL::SSL::SSLContext.stub(:new).and_return(@ctx_mock) From 7a62ec94182e2a658ee893cee2a715a7f496ce3c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 10:41:57 -0700 Subject: [PATCH 023/952] Convert specs to RSpec 2.99.1 syntax with Transpec This conversion is done by Transpec 2.3.6 with the following command: transpec * 579 conversions from: obj.should to: expect(obj).to * 498 conversions from: == expected to: eq(expected) * 143 conversions from: obj.stub(:message) to: allow(obj).to receive(:message) * 108 conversions from: obj.should_receive(:message) to: expect(obj).to receive(:message) * 25 conversions from: lambda { }.should to: expect { }.to * 12 conversions from: Klass.any_instance.should_receive(:message) to: expect_any_instance_of(Klass).to receive(:message) * 9 conversions from: obj.should_not_receive(:message) to: expect(obj).not_to receive(:message) * 5 conversions from: =~ /pattern/ to: match(/pattern/) * 5 conversions from: obj.should_not to: expect(obj).not_to * 4 conversions from: lambda { }.should_not to: expect { }.not_to * 2 conversions from: Klass.any_instance.stub(:message) to: allow_any_instance_of(Klass).to receive(:message) * 1 conversion from: >= expected to: be >= expected For more details: https://github.com/yujinakayama/transpec#supported-conversions --- spec/helper/pg_dump_restore_spec.rb | 16 +- spec/heroku/auth_spec.rb | 136 +++++------ spec/heroku/client/heroku_postgresql_spec.rb | 14 +- spec/heroku/client/pgbackups_spec.rb | 10 +- spec/heroku/client/rendezvous_spec.rb | 38 +-- spec/heroku/client/ssl_endpoint_spec.rb | 12 +- spec/heroku/client_spec.rb | 140 +++++------ spec/heroku/command/addons_spec.rb | 224 +++++++++--------- spec/heroku/command/apps_spec.rb | 98 ++++---- spec/heroku/command/auth_spec.rb | 16 +- spec/heroku/command/base_spec.rb | 80 +++---- spec/heroku/command/certs_spec.rb | 56 ++--- spec/heroku/command/config_spec.rb | 48 ++-- spec/heroku/command/domains_spec.rb | 24 +- spec/heroku/command/drains_spec.rb | 12 +- spec/heroku/command/fork_spec.rb | 25 +- spec/heroku/command/git_spec.rb | 32 +-- spec/heroku/command/help_spec.rb | 70 +++--- spec/heroku/command/keys_spec.rb | 44 ++-- spec/heroku/command/labs_spec.rb | 32 +-- spec/heroku/command/logs_spec.rb | 12 +- spec/heroku/command/maintenance_spec.rb | 16 +- spec/heroku/command/orgs_spec.rb | 44 ++-- spec/heroku/command/pg_spec.rb | 50 ++-- spec/heroku/command/pgbackups_spec.rb | 104 ++++---- spec/heroku/command/plugins_spec.rb | 50 ++-- spec/heroku/command/ps_spec.rb | 102 ++++---- spec/heroku/command/releases_spec.rb | 36 +-- spec/heroku/command/run_spec.rb | 20 +- spec/heroku/command/sharing_spec.rb | 16 +- spec/heroku/command/stack_spec.rb | 8 +- spec/heroku/command/status_spec.rb | 6 +- spec/heroku/command/version_spec.rb | 4 +- spec/heroku/command_spec.rb | 52 ++-- spec/heroku/helpers/heroku_postgresql_spec.rb | 68 +++--- spec/heroku/helpers_spec.rb | 12 +- spec/heroku/plugin_spec.rb | 58 ++--- spec/spec_helper.rb | 12 +- spec/support/openssl_mock_helper.rb | 4 +- 39 files changed, 901 insertions(+), 900 deletions(-) diff --git a/spec/helper/pg_dump_restore_spec.rb b/spec/helper/pg_dump_restore_spec.rb index 5e8ed5c9b..761c51063 100644 --- a/spec/helper/pg_dump_restore_spec.rb +++ b/spec/helper/pg_dump_restore_spec.rb @@ -19,18 +19,18 @@ it 'on pulls, prepare requires the local database to not exist' do mock_command = double - mock_command.should_receive(:error).once + expect(mock_command).to receive(:error).once pgdr = PgDumpRestore.new(@remotedb, @localdb, mock_command) - pgdr.should_receive(:`).once.and_return(`false`) + expect(pgdr).to receive(:`).once.and_return(`false`) pgdr.prepare end it 'on pushes, prepare requires the remote database to be empty' do mock_command = double - mock_command.should_receive(:error).once + expect(mock_command).to receive(:error).once pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) - mock_command.should_receive(:exec_sql_on_uri).once.and_return("something that isn't a true") + expect(mock_command).to receive(:exec_sql_on_uri).once.and_return("something that isn't a true") pgdr.prepare end @@ -50,17 +50,17 @@ describe 'verification' do it 'errors when the extensions do not match' do mock_command = double - mock_command.should_receive(:error).once + expect(mock_command).to receive(:error).once pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) - mock_command.should_receive(:exec_sql_on_uri).twice.and_return("these", "don't match") + expect(mock_command).to receive(:exec_sql_on_uri).twice.and_return("these", "don't match") pgdr.verify end it 'is fine when the extensions match' do mock_command = double - mock_command.should_not_receive(:error) + expect(mock_command).not_to receive(:error) pgdr = PgDumpRestore.new(@localdb, @remotedb, mock_command) - mock_command.should_receive(:exec_sql_on_uri).twice.and_return("these match", "these match") + expect(mock_command).to receive(:exec_sql_on_uri).twice.and_return("these match", "these match") pgdr.verify end end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index 6bb48b3e7..9da71e573 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -10,16 +10,16 @@ module Heroku ENV['HEROKU_API_KEY'] = nil @cli = Heroku::Auth - @cli.stub(:check) - @cli.stub(:display) - @cli.stub(:running_on_a_mac?).and_return(false) + allow(@cli).to receive(:check) + allow(@cli).to receive(:display) + allow(@cli).to receive(:running_on_a_mac?).and_return(false) @cli.credentials = nil FakeFS.activate! - FakeFS::File.stub(:stat).and_return(double('stat', :mode => "0600".to_i(8))) - FakeFS::FileUtils.stub(:chmod) - FakeFS::File.stub(:readlines) do |path| + allow(FakeFS::File).to receive(:stat).and_return(double('stat', :mode => "0600".to_i(8))) + allow(FakeFS::FileUtils).to receive(:chmod) + allow(FakeFS::File).to receive(:readlines) do |path| File.read(path).split("\n").map {|line| "#{line}\n"} end @@ -48,16 +48,16 @@ module Heroku it "should translate to netrc and cleanup" do # preconditions - File.exist?(@cli.legacy_credentials_path).should == true - File.exist?(@cli.netrc_path).should == false + expect(File.exist?(@cli.legacy_credentials_path)).to eq(true) + expect(File.exist?(@cli.netrc_path)).to eq(false) # transition - @cli.get_credentials.should == ['legacy_user', 'legacy_pass'] + expect(@cli.get_credentials).to eq(['legacy_user', 'legacy_pass']) # postconditions - File.exist?(@cli.legacy_credentials_path).should == false - File.exist?(@cli.netrc_path).should == true - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == ['legacy_user', 'legacy_pass'] + expect(File.exist?(@cli.legacy_credentials_path)).to eq(false) + expect(File.exist?(@cli.netrc_path)).to eq(true) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['legacy_user', 'legacy_pass']) end end @@ -67,34 +67,34 @@ module Heroku end it "gets credentials from environment variables in preference to credentials file" do - @cli.read_credentials.should == ['', ENV['HEROKU_API_KEY']] + expect(@cli.read_credentials).to eq(['', ENV['HEROKU_API_KEY']]) end it "returns a blank username" do - @cli.user.should be_empty + expect(@cli.user).to be_empty end it "returns the api key as the password" do - @cli.password.should == ENV['HEROKU_API_KEY'] + expect(@cli.password).to eq(ENV['HEROKU_API_KEY']) end it "does not overwrite credentials file with environment variable credentials" do - @cli.should_not_receive(:write_credentials) + expect(@cli).not_to receive(:write_credentials) @cli.read_credentials end context "reauthenticating" do before do - @cli.stub(:ask_for_credentials).and_return(['new_user', 'new_password']) - @cli.stub(:check) - @cli.should_receive(:check_for_associated_ssh_key) + allow(@cli).to receive(:ask_for_credentials).and_return(['new_user', 'new_password']) + allow(@cli).to receive(:check) + expect(@cli).to receive(:check_for_associated_ssh_key) @cli.reauthorize end it "updates saved credentials" do - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == ['new_user', 'new_password'] + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['new_user', 'new_password']) end it "returns environment variable credentials" do - @cli.read_credentials.should == ['', ENV['HEROKU_API_KEY']] + expect(@cli.read_credentials).to eq(['', ENV['HEROKU_API_KEY']]) end end @@ -103,56 +103,56 @@ module Heroku @cli.logout end it "should delete saved credentials" do - File.exists?(@cli.legacy_credentials_path).should be_falsey - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should be_nil + expect(File.exists?(@cli.legacy_credentials_path)).to be_falsey + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to be_nil end end end describe "#base_host" do it "returns the host without the first part" do - @cli.base_host("http://foo.bar.com").should == "bar.com" + expect(@cli.base_host("http://foo.bar.com")).to eq("bar.com") end it "works with localhost" do - @cli.base_host("http://localhost:3000").should == "localhost" + expect(@cli.base_host("http://localhost:3000")).to eq("localhost") end end it "asks for credentials when the file doesn't exist" do @cli.delete_credentials - @cli.should_receive(:ask_for_credentials).and_return(["u", "p"]) - @cli.should_receive(:check_for_associated_ssh_key) - @cli.user.should == 'u' - @cli.password.should == 'p' + expect(@cli).to receive(:ask_for_credentials).and_return(["u", "p"]) + expect(@cli).to receive(:check_for_associated_ssh_key) + expect(@cli.user).to eq('u') + expect(@cli.password).to eq('p') end it "writes credentials and uploads authkey when credentials are saved" do - @cli.stub(:credentials) - @cli.stub(:check) - @cli.stub(:ask_for_credentials).and_return("username", "apikey") - @cli.should_receive(:write_credentials) - @cli.should_receive(:check_for_associated_ssh_key) + allow(@cli).to receive(:credentials) + allow(@cli).to receive(:check) + allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") + expect(@cli).to receive(:write_credentials) + expect(@cli).to receive(:check_for_associated_ssh_key) @cli.ask_for_and_save_credentials end it "save_credentials deletes the credentials when the upload authkey is unauthorized" do - @cli.stub(:write_credentials) - @cli.stub(:retry_login?).and_return(false) - @cli.stub(:ask_for_credentials).and_return("username", "apikey") - @cli.stub(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } - @cli.should_receive(:delete_credentials) - lambda { @cli.ask_for_and_save_credentials }.should raise_error(SystemExit) + allow(@cli).to receive(:write_credentials) + allow(@cli).to receive(:retry_login?).and_return(false) + allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") + allow(@cli).to receive(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } + expect(@cli).to receive(:delete_credentials) + expect { @cli.ask_for_and_save_credentials }.to raise_error(SystemExit) end it "asks for login again when not authorized, for three times" do - @cli.stub(:read_credentials) - @cli.stub(:write_credentials) - @cli.stub(:delete_credentials) - @cli.stub(:ask_for_credentials).and_return("username", "apikey") - @cli.stub(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } - @cli.should_receive(:ask_for_credentials).exactly(3).times - lambda { @cli.ask_for_and_save_credentials }.should raise_error(SystemExit) + allow(@cli).to receive(:read_credentials) + allow(@cli).to receive(:write_credentials) + allow(@cli).to receive(:delete_credentials) + allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") + allow(@cli).to receive(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } + expect(@cli).to receive(:ask_for_credentials).exactly(3).times + expect { @cli.ask_for_and_save_credentials }.to raise_error(SystemExit) end it "deletes the credentials file" do @@ -160,16 +160,16 @@ module Heroku File.open(@cli.legacy_credentials_path, "w") do |file| file.puts "legacy_user\nlegacy_pass" end - FileUtils.should_receive(:rm_f).with(@cli.legacy_credentials_path) + expect(FileUtils).to receive(:rm_f).with(@cli.legacy_credentials_path) @cli.delete_credentials end it "writes the login information to the credentials file for the 'heroku login' command" do - @cli.stub(:ask_for_credentials).and_return(['one', 'two']) - @cli.stub(:check) - @cli.should_receive(:check_for_associated_ssh_key) + allow(@cli).to receive(:ask_for_credentials).and_return(['one', 'two']) + allow(@cli).to receive(:check) + expect(@cli).to receive(:check_for_associated_ssh_key) @cli.reauthorize - Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].should == (['one', 'two']) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['one', 'two']) end it "migrates long api keys to short api keys" do @@ -177,29 +177,29 @@ module Heroku api_key = "7e262de8cac430d8a250793ce8d5b334ae56b4ff15767385121145198a2b4d2e195905ef8bf7cfc5" @cli.netrc["api.#{@cli.host}"] = ["user", api_key] - @cli.get_credentials.should == ["user", api_key[0,40]] + expect(@cli.get_credentials).to eq(["user", api_key[0,40]]) %w{api code}.each do |section| - Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"].should == ["user", api_key[0,40]] + expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"]).to eq(["user", api_key[0,40]]) end end describe "automatic key uploading" do before(:each) do FileUtils.mkdir_p("#{@cli.home_directory}/.ssh") - @cli.stub(:ask_for_credentials).and_return("username", "apikey") + allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") end describe "an account with existing keys" do before :each do @api = double(Object) @response = double(Object) - @response.should_receive(:body).and_return(['existingkeys']) - @api.should_receive(:get_keys).and_return(@response) - @cli.should_receive(:api).and_return(@api) + expect(@response).to receive(:body).and_return(['existingkeys']) + expect(@api).to receive(:get_keys).and_return(@response) + expect(@cli).to receive(:api).and_return(@api) end it "should not do anything if the account already has keys" do - @cli.should_not_receive(:associate_key) + expect(@cli).not_to receive(:associate_key) @cli.check_for_associated_ssh_key end end @@ -208,16 +208,16 @@ module Heroku before :each do @api = double(Object) @response = double(Object) - @response.should_receive(:body).and_return([]) - @api.should_receive(:get_keys).and_return(@response) - @cli.should_receive(:api).and_return(@api) + expect(@response).to receive(:body).and_return([]) + expect(@api).to receive(:get_keys).and_return(@response) + expect(@cli).to receive(:api).and_return(@api) end describe "with zero public keys" do it "should ask to generate a key" do - @cli.should_receive(:ask).and_return("y") - @cli.should_receive(:generate_ssh_key).with("id_rsa") - @cli.should_receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") + expect(@cli).to receive(:ask).and_return("y") + expect(@cli).to receive(:generate_ssh_key).with("id_rsa") + expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") @cli.check_for_associated_ssh_key end end @@ -227,7 +227,7 @@ module Heroku after(:each) { FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa.pub") } it "should upload the key" do - @cli.should_receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") + expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") @cli.check_for_associated_ssh_key end end @@ -245,8 +245,8 @@ module Heroku it "should ask which key to upload" do File.open("#{@cli.home_directory}/.ssh/id_rsa.pub", "w") { |f| f.puts } - @cli.should_receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa2.pub") - @cli.should_receive(:ask).and_return("2") + expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa2.pub") + expect(@cli).to receive(:ask).and_return("2") @cli.check_for_associated_ssh_key end end diff --git a/spec/heroku/client/heroku_postgresql_spec.rb b/spec/heroku/client/heroku_postgresql_spec.rb index 7e13e4754..65a36a3a5 100644 --- a/spec/heroku/client/heroku_postgresql_spec.rb +++ b/spec/heroku/client/heroku_postgresql_spec.rb @@ -25,7 +25,7 @@ client.ingress - a_request(:put, url).should have_been_made.once + expect(a_request(:put, url)).to have_been_made.once end it "sends an ingress request to the client for production plans" do @@ -40,7 +40,7 @@ client.ingress - a_request(:put, url).should have_been_made.once + expect(a_request(:put, url)).to have_been_made.once end end @@ -50,21 +50,21 @@ it 'works without the extended option' do stub_request(:get, url).to_return :body => '{}' client.get_database - a_request(:get, url).should have_been_made.once + expect(a_request(:get, url)).to have_been_made.once end it 'works with the extended option' do url2 = url + '?extended=true' stub_request(:get, url2).to_return :body => '{}' client.get_database(true) - a_request(:get, url2).should have_been_made.once + expect(a_request(:get, url2)).to have_been_made.once end it "retries on error, then raises" do stub_request(:get, url).to_return(:body => "error", :status => 500) - client.stub(:sleep) - lambda { client.get_database }.should raise_error RestClient::InternalServerError - a_request(:get, url).should have_been_made.times(4) + allow(client).to receive(:sleep) + expect { client.get_database }.to raise_error RestClient::InternalServerError + expect(a_request(:get, url)).to have_been_made.times(4) end end diff --git a/spec/heroku/client/pgbackups_spec.rb b/spec/heroku/client/pgbackups_spec.rb index b6b7cdfb2..e4fa794cf 100644 --- a/spec/heroku/client/pgbackups_spec.rb +++ b/spec/heroku/client/pgbackups_spec.rb @@ -14,16 +14,16 @@ let(:version) { Heroku::Client.version } it 'still has a heroku gem version' do - version.should be - version.split(/\./).first.to_i.should >= 2 + expect(version).to be + expect(version.split(/\./).first.to_i).to be >= 2 end it 'includes the heroku gem version' do stub_request(:get, transfer_path) client.get_transfers - a_request(:get, transfer_path).with( + expect(a_request(:get, transfer_path).with( :headers => {'X-Heroku-Gem-Version' => version} - ).should have_been_made.once + )).to have_been_made.once end end @@ -36,7 +36,7 @@ client.create_transfer("postgres://from", "postgres://to", "FROMNAME", "TO_NAME") - a_request(:post, transfer_path).should have_been_made.once + expect(a_request(:post, transfer_path)).to have_been_made.once end end diff --git a/spec/heroku/client/rendezvous_spec.rb b/spec/heroku/client/rendezvous_spec.rb index f450ec98a..1cc73f7b8 100644 --- a/spec/heroku/client/rendezvous_spec.rb +++ b/spec/heroku/client/rendezvous_spec.rb @@ -12,51 +12,51 @@ end context "fixup" do it "null" do - @rendezvous.send(:fixup, nil).should be_nil + expect(@rendezvous.send(:fixup, nil)).to be_nil end it "an empty string" do - @rendezvous.send(:fixup, "").should eq "" + expect(@rendezvous.send(:fixup, "")).to eq "" end it "hash" do - @rendezvous.send(:fixup, { :x => :y }).should eq({ :x => :y }) + expect(@rendezvous.send(:fixup, { :x => :y })).to eq({ :x => :y }) end it "default English UTF-8 data" do - @rendezvous.send(:fixup, "heroku").should eq "heroku" + expect(@rendezvous.send(:fixup, "heroku")).to eq "heroku" end it "default Japanese UTF-8 encoded data" do - @rendezvous.send(:fixup, "愛しています").should eq "愛しています" + expect(@rendezvous.send(:fixup, "愛しています")).to eq "愛しています" end if RUBY_VERSION >= "1.9" it "ISO-8859-1 force-encoded data" do - @rendezvous.send(:fixup, "Хероку".force_encoding("ISO-8859-1")).should eq "Хероку".force_encoding("UTF-8") + expect(@rendezvous.send(:fixup, "Хероку".force_encoding("ISO-8859-1"))).to eq "Хероку".force_encoding("UTF-8") end end end context "with mock ssl" do before :each do mock_openssl - @ssl_socket_mock.should_receive(:puts).with("secret") - @ssl_socket_mock.should_receive(:readline).and_return(nil) + expect(@ssl_socket_mock).to receive(:puts).with("secret") + expect(@ssl_socket_mock).to receive(:readline).and_return(nil) end it "should connect to host:post" do - TCPSocket.should_receive(:open).with("heroku.local", 1234).and_return(@tcp_socket_mock) - IO.stub(:select).and_return(nil) - @ssl_socket_mock.stub(:write) - @ssl_socket_mock.stub(:flush) { raise Timeout::Error } - lambda { @rendezvous.start }.should raise_error(Timeout::Error) + expect(TCPSocket).to receive(:open).with("heroku.local", 1234).and_return(@tcp_socket_mock) + allow(IO).to receive(:select).and_return(nil) + allow(@ssl_socket_mock).to receive(:write) + allow(@ssl_socket_mock).to receive(:flush) { raise Timeout::Error } + expect { @rendezvous.start }.to raise_error(Timeout::Error) end it "should callback on_connect" do @rendezvous.on_connect do raise "on_connect" end - TCPSocket.should_receive(:open).and_return(@tcp_socket_mock) - lambda { @rendezvous.start }.should raise_error("on_connect") + expect(TCPSocket).to receive(:open).and_return(@tcp_socket_mock) + expect { @rendezvous.start }.to raise_error("on_connect") end it "should fixup received data" do - TCPSocket.should_receive(:open).and_return(@tcp_socket_mock) - @ssl_socket_mock.should_receive(:readpartial).and_return("The quick brown fox jumps over the lazy dog") - @rendezvous.stub(:fixup) { |data| raise "received: #{data}" } - lambda { @rendezvous.start }.should raise_error("received: The quick brown fox jumps over the lazy dog") + expect(TCPSocket).to receive(:open).and_return(@tcp_socket_mock) + expect(@ssl_socket_mock).to receive(:readpartial).and_return("The quick brown fox jumps over the lazy dog") + allow(@rendezvous).to receive(:fixup) { |data| raise "received: #{data}" } + expect { @rendezvous.start }.to raise_error("received: The quick brown fox jumps over the lazy dog") end end end diff --git a/spec/heroku/client/ssl_endpoint_spec.rb b/spec/heroku/client/ssl_endpoint_spec.rb index d58f1e1cb..a78abf49d 100644 --- a/spec/heroku/client/ssl_endpoint_spec.rb +++ b/spec/heroku/client/ssl_endpoint_spec.rb @@ -10,22 +10,22 @@ stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_add("example", "pem content", "key content").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_add("example", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) end it "gets info on an ssl endpoint" do stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_info("example", "tokyo-1050").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_info("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "lists ssl endpoints for an app" do stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints"). to_return(:body => %{ [{"cname": "tokyo-1050" }, {"cname": "tokyo-1051" }] }) - @client.ssl_endpoint_list("example").should == [ + expect(@client.ssl_endpoint_list("example")).to eq([ { "cname" => "tokyo-1050" }, { "cname" => "tokyo-1051" }, - ] + ]) end it "removes an ssl endpoint" do @@ -36,13 +36,13 @@ it "rolls back an ssl endpoint" do stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050/rollback"). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_rollback("example", "tokyo-1050").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_rollback("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "updates an ssl endpoint" do stub_request(:put, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) - @client.ssl_endpoint_update("example", "tokyo-1050", "pem content", "key content").should == { "cname" => "tokyo-1050" } + expect(@client.ssl_endpoint_update("example", "tokyo-1050", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) end end diff --git a/spec/heroku/client_spec.rb b/spec/heroku/client_spec.rb index 2af5c5342..d9a8826a9 100644 --- a/spec/heroku/client_spec.rb +++ b/spec/heroku/client_spec.rb @@ -9,14 +9,14 @@ before do @client = Heroku::Client.new(nil, nil) @resource = double('heroku rest resource') - @client.stub(:extract_warning) + allow(@client).to receive(:extract_warning) end it "Client.auth -> get user details" do user_info = { "api_key" => "abc" } stub_request(:post, "https://foo:bar@api.heroku.com/login").to_return(:body => json_encode(user_info)) capture_stderr do # capture deprecation message - Heroku::Client.auth("foo", "bar").should == user_info + expect(Heroku::Client.auth("foo", "bar")).to eq(user_info) end end @@ -29,10 +29,10 @@ EOXML capture_stderr do # capture deprecation message - @client.list.should == [ + expect(@client.list).to eq([ ["example", "test@heroku.com"], ["example2", "test@heroku.com"] - ] + ]) end end @@ -49,10 +49,10 @@ EOXML - @client.stub(:list_collaborators).and_return([:jon, :mike]) - @client.stub(:installed_addons).and_return([:addon1]) + allow(@client).to receive(:list_collaborators).and_return([:jon, :mike]) + allow(@client).to receive(:installed_addons).and_return([:addon1]) capture_stderr do # capture deprecation message - @client.info('example').should == { :blessed => 'true', :created_at => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'example', :production => 'true', :share_public => 'true', :domain_name => nil, :collaborators => [:jon, :mike], :addons => [:addon1] } + expect(@client.info('example')).to eq({ :blessed => 'true', :created_at => '2008-07-08T17:21:50-07:00', :id => '49134', :name => 'example', :production => 'true', :share_public => 'true', :domain_name => nil, :collaborators => [:jon, :mike], :addons => [:addon1] }) end end @@ -62,7 +62,7 @@ untitled-123 EOXML capture_stderr do # capture deprecation message - @client.create_request.should == "untitled-123" + expect(@client.create_request).to eq("untitled-123") end end @@ -72,17 +72,17 @@ newapp EOXML capture_stderr do # capture deprecation message - @client.create_request("newapp").should == "newapp" + expect(@client.create_request("newapp")).to eq("newapp") end end it "create_complete?(name) -> checks if a create request is complete" do @response = double('response') - @response.should_receive(:code).and_return(202) - @client.should_receive(:resource).and_return(@resource) - @resource.should_receive(:put).with({}, @client.heroku_headers).and_return(@response) + expect(@response).to receive(:code).and_return(202) + expect(@client).to receive(:resource).and_return(@resource) + expect(@resource).to receive(:put).with({}, @client.heroku_headers).and_return(@response) capture_stderr do # capture deprecation message - @client.create_complete?('example').should be_falsey + expect(@client.create_complete?('example')).to be_falsey end end @@ -119,7 +119,7 @@ stub_api_request(:delete, "/apps/example/consoles/consolename") @client.console('example') do |c| - c.run("1+1").should == '=> 2' + expect(c.run("1+1")).to eq('=> 2') end end @@ -127,7 +127,7 @@ stub_request(:post, %r{.*/apps/example/console}).to_return({ :body => "ERRMSG", :status => 502 }) - lambda { @client.console('example') }.should raise_error(Heroku::Client::AppCrashed, /Your application may have crashed/) + expect { @client.console('example') }.to raise_error(Heroku::Client::AppCrashed, /Your application may have crashed/) end it "restart(app_name) -> restarts the app servers" do @@ -145,7 +145,7 @@ end it "can read old style logs" do - @client.should_receive(:puts).with("oldlogs") + expect(@client).to receive(:puts).with("oldlogs") @client.read_logs("example") end end @@ -158,7 +158,7 @@ it "can read new style logs" do @client.read_logs("example") do |logs| - logs.should == "newlogs" + expect(logs).to eq("newlogs") end end end @@ -167,7 +167,7 @@ it "logs(app_name) -> returns recent output of the app logs" do stub_api_request(:get, "/apps/example/logs").to_return(:body => "log") capture_stderr do # capture deprecation message - @client.logs('example').should == 'log' + expect(@client.logs('example')).to eq('log') end end @@ -179,7 +179,7 @@ EOXML capture_stderr do # capture deprecation message - @client.dynos('example').should == 5 + expect(@client.dynos('example')).to eq(5) end end @@ -191,7 +191,7 @@ EOXML capture_stderr do # capture deprecation message - @client.workers('example').should == 5 + expect(@client.workers('example')).to eq(5) end end @@ -204,21 +204,21 @@ it "rake catches 502s and shows the app crashlog" do e = RestClient::RequestFailed.new - e.stub(:http_code).and_return(502) - e.stub(:http_body).and_return('the crashlog') - @client.should_receive(:post).and_raise(e) + allow(e).to receive(:http_code).and_return(502) + allow(e).to receive(:http_body).and_return('the crashlog') + expect(@client).to receive(:post).and_raise(e) capture_stderr do # capture deprecation message - lambda { @client.rake('example', '') }.should raise_error(Heroku::Client::AppCrashed) + expect { @client.rake('example', '') }.to raise_error(Heroku::Client::AppCrashed) end end it "rake passes other status codes (i.e., 500) as standard restclient exceptions" do e = RestClient::RequestFailed.new - e.stub(:http_code).and_return(500) - e.stub(:http_body).and_return('not a crashlog') - @client.should_receive(:post).and_raise(e) + allow(e).to receive(:http_code).and_return(500) + allow(e).to receive(:http_body).and_return('not a crashlog') + expect(@client).to receive(:post).and_raise(e) capture_stderr do # capture deprecation message - lambda { @client.rake('example', '') }.should raise_error(RestClient::RequestFailed) + expect { @client.rake('example', '') }.to raise_error(RestClient::RequestFailed) end end @@ -226,7 +226,7 @@ it "scales a process and returns the new count" do stub_api_request(:post, "/apps/example/ps/scale").with(:body => { :type => "web", :qty => "5" }).to_return(:body => "5") capture_stderr do # capture deprecation message - @client.ps_scale("example", :type => "web", :qty => "5").should == 5 + expect(@client.ps_scale("example", :type => "web", :qty => "5")).to eq(5) end end end @@ -241,10 +241,10 @@ EOXML capture_stderr do # capture deprecation message - @client.list_collaborators('example').should == [ + expect(@client.list_collaborators('example')).to eq([ { :email => 'joe@example.com' }, { :email => 'jon@example.com' } - ] + ]) end end @@ -273,7 +273,7 @@ EOXML capture_stderr do # capture deprecation message - @client.list_domains('example').should == [{:domain => 'example1.com'}, {:domain => 'example2.com'}] + expect(@client.list_domains('example')).to eq([{:domain => 'example1.com'}, {:domain => 'example2.com'}]) end end @@ -292,11 +292,11 @@ end it "remove_domain(app_name, domain) -> makes sure a domain is set" do - lambda do + expect do capture_stderr do # capture deprecation message @client.remove_domain('example', '') end - end.should raise_error(ArgumentError) + end.to raise_error(ArgumentError) end it "remove_domains(app_name) -> removes all domain names from app" do @@ -309,8 +309,8 @@ it "add_ssl(app_name, pem, key) -> adds a ssl cert to the domain" do stub_api_request(:post, "/apps/example/ssl").with do |request| body = CGI::parse(request.body) - body["key"].first.should == "thekey" - body["pem"].first.should == "thepem" + expect(body["key"].first).to eq("thekey") + expect(body["pem"].first).to eq("thepem") end.to_return(:body => "{}") @client.add_ssl('example', 'thepem', 'thekey') end @@ -332,7 +332,7 @@ EOXML capture_stderr do # capture deprecation message - @client.keys.should == [ "ssh-dss thekey== joe@workstation" ] + expect(@client.keys).to eq([ "ssh-dss thekey== joe@workstation" ]) end end @@ -376,7 +376,7 @@ it "config_vars(app_name) -> json hash of config vars for the app" do stub_api_request(:get, "/apps/example/config_vars").to_return(:body => '{"A":"one", "B":"two"}') capture_stderr do # capture deprecation message - @client.config_vars('example').should == { 'A' => 'one', 'B' => 'two'} + expect(@client.config_vars('example')).to eq({ 'A' => 'one', 'B' => 'two'}) end end @@ -404,7 +404,7 @@ it "can handle config vars with special characters" do stub_api_request(:delete, "/apps/example/config_vars/foo%5Bbar%5D") capture_stderr do # capture deprecation message - lambda { @client.remove_config_var('example', 'foo[bar]') }.should_not raise_error + expect { @client.remove_config_var('example', 'foo[bar]') }.not_to raise_error end end end @@ -412,73 +412,73 @@ describe "addons" do it "addons -> array with addons available for installation" do stub_api_request(:get, "/addons").to_return(:body => '[{"name":"addon1"}, {"name":"addon2"}]') - @client.addons.should == [{'name' => 'addon1'}, {'name' => 'addon2'}] + expect(@client.addons).to eq([{'name' => 'addon1'}, {'name' => 'addon2'}]) end it "installed_addons(app_name) -> array of installed addons" do stub_api_request(:get, "/apps/example/addons").to_return(:body => '[{"name":"addon1"}]') - @client.installed_addons('example').should == [{'name' => 'addon1'}] + expect(@client.installed_addons('example')).to eq([{'name' => 'addon1'}]) end it "install_addon(app_name, addon_name)" do stub_api_request(:post, "/apps/example/addons/addon1") - @client.install_addon('example', 'addon1').should be_nil + expect(@client.install_addon('example', 'addon1')).to be_nil end it "upgrade_addon(app_name, addon_name)" do stub_api_request(:put, "/apps/example/addons/addon1") - @client.upgrade_addon('example', 'addon1').should be_nil + expect(@client.upgrade_addon('example', 'addon1')).to be_nil end it "downgrade_addon(app_name, addon_name)" do stub_api_request(:put, "/apps/example/addons/addon1") - @client.downgrade_addon('example', 'addon1').should be_nil + expect(@client.downgrade_addon('example', 'addon1')).to be_nil end it "uninstall_addon(app_name, addon_name)" do stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) - @client.uninstall_addon('example', 'addon1').should be_truthy + expect(@client.uninstall_addon('example', 'addon1')).to be_truthy end it "uninstall_addon(app_name, addon_name) with confirmation" do stub_api_request(:delete, "/apps/example/addons/addon1?confirm=example"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) - @client.uninstall_addon('example', 'addon1', :confirm => "example").should be_truthy + expect(@client.uninstall_addon('example', 'addon1', :confirm => "example")).to be_truthy end it "install_addon(app_name, addon_name) with response" do stub_request(:post, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode({'price' => 'free', 'message' => "Don't Panic"})) - @client.install_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.install_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end it "upgrade_addon(app_name, addon_name) with response" do stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) - @client.upgrade_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.upgrade_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end it "downgrade_addon(app_name, addon_name) with response" do stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) - @client.downgrade_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.downgrade_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end it "uninstall_addon(app_name, addon_name) with response" do stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode('price'=> 'free', 'message'=> "Don't Panic")) - @client.uninstall_addon('example', 'addon1'). - should == { 'price' => 'free', 'message' => "Don't Panic" } + expect(@client.uninstall_addon('example', 'addon1')). + to eq({ 'price' => 'free', 'message' => "Don't Panic" }) end end @@ -488,45 +488,45 @@ end it "creates a RestClient resource for making calls" do - @client.stub(:host).and_return('heroku.com') - @client.stub(:user).and_return('joe@example.com') - @client.stub(:password).and_return('secret') + allow(@client).to receive(:host).and_return('heroku.com') + allow(@client).to receive(:user).and_return('joe@example.com') + allow(@client).to receive(:password).and_return('secret') res = @client.resource('/xyz') - res.url.should == 'https://api.heroku.com/xyz' - res.user.should == 'joe@example.com' - res.password.should == 'secret' + expect(res.url).to eq('https://api.heroku.com/xyz') + expect(res.user).to eq('joe@example.com') + expect(res.password).to eq('secret') end it "appends the api. prefix to the host" do @client.host = "heroku.com" - @client.resource('/xyz').url.should == 'https://api.heroku.com/xyz' + expect(@client.resource('/xyz').url).to eq('https://api.heroku.com/xyz') end it "doesn't add the api. prefix to full hosts" do @client.host = 'http://resource' res = @client.resource('/xyz') - res.url.should == 'http://resource/xyz' + expect(res.url).to eq('http://resource/xyz') end it "runs a callback when the API sets a warning header" do response = double('rest client response', :headers => { :x_heroku_warning => 'Warning' }) - @client.should_receive(:resource).and_return(@resource) - @resource.should_receive(:get).and_return(response) + expect(@client).to receive(:resource).and_return(@resource) + expect(@resource).to receive(:get).and_return(response) @client.on_warning { |msg| @callback = msg } @client.get('test') - @callback.should == 'Warning' + expect(@callback).to eq('Warning') end it "doesn't run the callback twice for the same warning" do response = double('rest client response', :headers => { :x_heroku_warning => 'Warning' }) - @client.stub(:resource).and_return(@resource) - @resource.stub(:get).and_return(response) + allow(@client).to receive(:resource).and_return(@resource) + allow(@resource).to receive(:get).and_return(response) @client.on_warning { |msg| @callback_called ||= 0; @callback_called += 1 } @client.get('test1') @client.get('test2') - @callback_called.should == 1 + expect(@callback_called).to eq(1) end end @@ -534,14 +534,14 @@ it "list_stacks(app_name) -> json hash of available stacks" do stub_api_request(:get, "/apps/example/stack?include_deprecated=false").to_return(:body => '{"stack":"one"}') capture_stderr do # capture deprecation message - @client.list_stacks("example").should == { 'stack' => 'one' } + expect(@client.list_stacks("example")).to eq({ 'stack' => 'one' }) end end it "list_stacks(app_name, include_deprecated=true) passes the deprecated option" do stub_api_request(:get, "/apps/example/stack?include_deprecated=true").to_return(:body => '{"stack":"one"}') capture_stderr do # capture deprecation message - @client.list_stacks("example", :include_deprecated => true).should == { 'stack' => 'one' } + expect(@client.list_stacks("example", :include_deprecated => true)).to eq({ 'stack' => 'one' }) end end end diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index a29941e00..ab59a054f 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -21,8 +21,8 @@ module Heroku::Command it "should display no addons when none are configured" do stderr, stdout = execute("addons") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT example has no add-ons. STDOUT end @@ -44,8 +44,8 @@ module Heroku::Command } ) stderr, stdout = execute("addons") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Configured Add-ons deployhooks:http heroku-postgresql:ronin HEROKU_POSTGRESQL_RED @@ -76,8 +76,8 @@ module Heroku::Command { "name" => "cloudcounter:platinum", "state" => "beta" } ]) stderr, stdout = execute("addons:list") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === alpha cloudcounter:basic @@ -96,8 +96,8 @@ module Heroku::Command describe 'v1-style command line params' do it "understands foo=baz" do - @addons.stub(:args).and_return(%w(my_addon foo=baz)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_addon foo=baz)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.add end @@ -117,8 +117,8 @@ module Heroku::Command } ) stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Warning: non-unix style params have been deprecated, use --extra=XXX instead Adding my_addon on example... done, v99 (free) Use `heroku addons:docs my_addon` to view documentation. @@ -129,32 +129,32 @@ module Heroku::Command describe 'unix-style command line params' do it "understands --foo=baz" do - @addons.stub(:args).and_return(%w(my_addon --foo=baz)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.add end it "understands --foo baz" do - @addons.stub(:args).and_return(%w(my_addon --foo baz)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo baz)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.add end it "treats lone switches as true" do - @addons.stub(:args).and_return(%w(my_addon --foo)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) @addons.add end it "converts 'true' to boolean" do - @addons.stub(:args).and_return(%w(my_addon --foo=true)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=true)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) @addons.add end it "works with many config vars" do - @addons.stub(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => true, 'bob' => true }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => true, 'bob' => true }) @addons.add end @@ -162,19 +162,19 @@ module Heroku::Command stub_request(:post, %r{apps/example/addons/my_addon$}). with(:body => {:config => { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => 'true', 'bob' => 'true' }}) stderr, stdout = execute("addons:add my_addon --foo baz --bar yes --baz=foo --bab --bob=true") - stderr.should == "" + expect(stderr).to eq("") end it "raises an error for spurious arguments" do - @addons.stub(:args).and_return(%w(my_addon spurious)) - lambda { @addons.add }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return(%w(my_addon spurious)) + expect { @addons.add }.to raise_error(CommandFailed) end end describe "mixed options" do it "understands foo=bar and --baz=bar on the same line" do - @addons.stub(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'baz' => 'bar', 'bar' => true, 'bob' => true }) + allow(@addons).to receive(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'baz' => 'bar', 'bar' => true, 'bob' => true }) @addons.add end @@ -182,16 +182,16 @@ module Heroku::Command stub_request(:post, %r{apps/example/addons/my_addon$}). with(:body => {:config => { 'foo' => 'baz', 'baz' => 'bar', 'bar' => 'true', 'bob' => 'true' }}) stderr, stdout = execute("addons:add my_addon foo=baz --baz=bar bob=true --bar") - stderr.should == "" - stdout.should include("Warning: non-unix style params have been deprecated, use --foo=baz --bob=true instead") + expect(stderr).to eq("") + expect(stdout).to include("Warning: non-unix style params have been deprecated, use --foo=baz --bob=true instead") end end describe "fork, follow, and rollback switches" do it "should only resolve for heroku-postgresql addon" do %w{fork follow rollback}.each do |switch| - @addons.stub(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) - @addons.heroku.should_receive(:install_addon). + allow(@addons).to receive(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) + expect(@addons.heroku).to receive(:install_addon). with('example', 'addon', {switch => 'HEROKU_POSTGRESQL_RED'}) @addons.add end @@ -199,8 +199,8 @@ module Heroku::Command it "should translate --fork, --follow, and --rollback" do %w{fork follow rollback}.each do |switch| - Heroku::Helpers::HerokuPostgresql::Resolver.any_instance.stub(:app_config_vars).and_return({}) - Heroku::Helpers::HerokuPostgresql::Resolver.any_instance.stub(:app_attachments).and_return([Heroku::Helpers::HerokuPostgresql::Attachment.new({ + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver).to receive(:app_config_vars).and_return({}) + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver).to receive(:app_attachments).and_return([Heroku::Helpers::HerokuPostgresql::Attachment.new({ 'app' => {'name' => 'sushi'}, 'name' => 'HEROKU_POSTGRESQL_RED', 'config_var' => 'HEROKU_POSTGRESQL_RED_URL', @@ -208,35 +208,35 @@ module Heroku::Command 'value' => 'postgres://red_url', 'type' => 'heroku-postgresql:ronin' }}) ]) - @addons.stub(:args).and_return("heroku-postgresql --#{switch} HEROKU_POSTGRESQL_RED".split) - @addons.heroku.should_receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://red_url'}) + allow(@addons).to receive(:args).and_return("heroku-postgresql --#{switch} HEROKU_POSTGRESQL_RED".split) + expect(@addons.heroku).to receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://red_url'}) @addons.add end end it "should NOT translate --fork and --follow if passed in a full postgres url even if there are no databases" do %w{fork follow}.each do |switch| - @addons.stub(:app_config_vars).and_return({}) - @addons.stub(:app_attachments).and_return([]) - @addons.stub(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) - @addons.heroku.should_receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://foo:yeah@awesome.com:234/bestdb'}) + allow(@addons).to receive(:app_config_vars).and_return({}) + allow(@addons).to receive(:app_attachments).and_return([]) + allow(@addons).to receive(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) + expect(@addons.heroku).to receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://foo:yeah@awesome.com:234/bestdb'}) @addons.add end end it "should fail if fork / follow across applications and no plan is specified" do %w{fork follow}.each do |switch| - @addons.stub(:app_config_vars).and_return({}) - @addons.stub(:app_attachments).and_return([]) - @addons.stub(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) - lambda { @addons.add }.should raise_error(CommandFailed) + allow(@addons).to receive(:app_config_vars).and_return({}) + allow(@addons).to receive(:app_attachments).and_return([]) + allow(@addons).to receive(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) + expect { @addons.add }.to raise_error(CommandFailed) end end end describe 'adding' do before do - @addons.stub(:args).and_return(%w(my_addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -255,28 +255,28 @@ module Heroku::Command it "requires an addon name" do - @addons.stub(:args).and_return([]) - lambda { @addons.add }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return([]) + expect { @addons.add }.to raise_error(CommandFailed) end it "adds an addon" do - @addons.stub(:args).and_return(%w(my_addon)) - @addons.heroku.should_receive(:install_addon).with('example', 'my_addon', {}) + allow(@addons).to receive(:args).and_return(%w(my_addon)) + expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', {}) @addons.add end it "adds an addon with a price" do stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "free" }) stderr, stdout = execute("addons:add my_addon") - stderr.should == "" - stdout.should =~ /\(free\)/ + expect(stderr).to eq("") + expect(stdout).to match(/\(free\)/) end it "adds an addon with a price and message" do stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "foo" }) stderr, stdout = execute("addons:add my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Adding my_addon on example... done, v99 (free) foo Use `heroku addons:docs my_addon` to view documentation. @@ -286,8 +286,8 @@ module Heroku::Command it "excludes addon plan from docs message" do stub_core.install_addon("example", "my_addon:test", {}).returns({ "price" => "free", "message" => "foo" }) stderr, stdout = execute("addons:add my_addon:test") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Adding my_addon:test on example... done, v99 (free) foo Use `heroku addons:docs my_addon` to view documentation. @@ -297,8 +297,8 @@ module Heroku::Command it "adds an addon with a price and multiline message" do stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "$200/mo", "message" => "foo\nbar" }) stderr, stdout = execute("addons:add my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Adding my_addon on example... done, v99 ($200/mo) foo bar @@ -307,14 +307,14 @@ module Heroku::Command end it "displays an error with unexpected options" do - Heroku::Command.should_receive(:error).with("Unexpected arguments: bar") + expect(Heroku::Command).to receive(:error).with("Unexpected arguments: bar") run("addons:add redistogo -a foo bar") end end describe 'upgrading' do before do - @addons.stub(:args).and_return(%w(my_addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -332,27 +332,27 @@ module Heroku::Command end it "requires an addon name" do - @addons.stub(:args).and_return([]) - lambda { @addons.upgrade }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return([]) + expect { @addons.upgrade }.to raise_error(CommandFailed) end it "upgrades an addon" do - @addons.stub(:args).and_return(%w(my_addon)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', {}) + allow(@addons).to receive(:args).and_return(%w(my_addon)) + expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', {}) @addons.upgrade end it "upgrade an addon with config vars" do - @addons.stub(:args).and_return(%w(my_addon --foo=baz)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) + expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.upgrade end it "adds an addon with a price" do stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free" }) stderr, stdout = execute("addons:upgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Upgrading to my_addon on example... done, v99 (free) Use `heroku addons:docs my_addon` to view documentation. OUTPUT @@ -361,8 +361,8 @@ module Heroku::Command it "adds an addon with a price and message" do stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "Don't Panic" }) stderr, stdout = execute("addons:upgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Upgrading to my_addon on example... done, v99 (free) Don't Panic Use `heroku addons:docs my_addon` to view documentation. @@ -372,7 +372,7 @@ module Heroku::Command describe 'downgrading' do before do - @addons.stub(:args).and_return(%w(my_addon)) + allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( { :expects => 200, @@ -390,27 +390,27 @@ module Heroku::Command end it "requires an addon name" do - @addons.stub(:args).and_return([]) - lambda { @addons.downgrade }.should raise_error(CommandFailed) + allow(@addons).to receive(:args).and_return([]) + expect { @addons.downgrade }.to raise_error(CommandFailed) end it "downgrades an addon" do - @addons.stub(:args).and_return(%w(my_addon)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', {}) + allow(@addons).to receive(:args).and_return(%w(my_addon)) + expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', {}) @addons.downgrade end it "downgrade an addon with config vars" do - @addons.stub(:args).and_return(%w(my_addon --foo=baz)) - @addons.heroku.should_receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) + expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.downgrade end it "downgrades an addon with a price" do stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free" }) stderr, stdout = execute("addons:downgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Downgrading to my_addon on example... done, v99 (free) Use `heroku addons:docs my_addon` to view documentation. OUTPUT @@ -419,8 +419,8 @@ module Heroku::Command it "downgrades an addon with a price and message" do stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "Don't Panic" }) stderr, stdout = execute("addons:downgrade my_addon") - stderr.should == "" - stdout.should == <<-OUTPUT + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT Downgrading to my_addon on example... done, v99 (free) Don't Panic Use `heroku addons:docs my_addon` to view documentation. @@ -429,23 +429,23 @@ module Heroku::Command end it "does not remove addons with no confirm" do - @addons.stub(:args).and_return(%w( addon1 )) - @addons.should_receive(:confirm_command).once.and_return(false) - @addons.heroku.should_not_receive(:uninstall_addon) + allow(@addons).to receive(:args).and_return(%w( addon1 )) + expect(@addons).to receive(:confirm_command).once.and_return(false) + expect(@addons.heroku).not_to receive(:uninstall_addon) @addons.remove end it "removes addons after prompting for confirmation" do - @addons.stub(:args).and_return(%w( addon1 )) - @addons.should_receive(:confirm_command).once.and_return(true) - @addons.heroku.should_receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") + allow(@addons).to receive(:args).and_return(%w( addon1 )) + expect(@addons).to receive(:confirm_command).once.and_return(true) + expect(@addons.heroku).to receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") @addons.remove end it "removes addons with confirm option" do - Heroku::Command.stub(:current_options).and_return(:confirm => "example") - @addons.stub(:args).and_return(%w( addon1 )) - @addons.heroku.should_receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "example") + allow(@addons).to receive(:args).and_return(%w( addon1 )) + expect(@addons.heroku).to receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") @addons.remove end @@ -462,19 +462,19 @@ module Heroku::Command it "displays usage when no argument is specified" do stderr, stdout = execute('addons:docs') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku addons:docs ADDON ! Must specify ADDON to open docs for. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "opens the addon if only one matches" do require("launchy") - Launchy.should_receive(:open).with("https://devcenter.heroku.com/articles/redistogo").and_return(Thread.new {}) + expect(Launchy).to receive(:open).with("https://devcenter.heroku.com/articles/redistogo").and_return(Thread.new {}) stderr, stdout = execute('addons:docs redistogo:nano') - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT Opening redistogo:nano docs... done STDOUT end @@ -495,39 +495,39 @@ module Heroku::Command } ) stderr, stdout = execute('addons:docs qu') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Ambiguous addon name: qu ! Perhaps you meant `qux:foo` or `quux:bar`. STDERR - stdout.should == '' + expect(stdout).to eq('') Excon.stubs.shift end it "complains if no such addon exists" do stderr, stdout = execute('addons:docs unknown') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! `unknown` is not a heroku add-on. ! See `heroku addons:list` for all available addons. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "suggests alternatives if addon has typo" do stderr, stdout = execute('addons:docs redisgoto') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! `redisgoto` is not a heroku add-on. ! Perhaps you meant `redistogo`. ! See `heroku addons:list` for all available addons. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "complains if addon is not installed" do stderr, stdout = execute('addons:open deployhooks:http') - stderr.should == <<-STDOUT + expect(stderr).to eq <<-STDOUT ! Addon not installed: deployhooks:http STDOUT - stdout.should == '' + expect(stdout).to eq('') end end describe "opening an addon" do @@ -543,20 +543,20 @@ module Heroku::Command it "displays usage when no argument is specified" do stderr, stdout = execute('addons:open') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku addons:open ADDON ! Must specify ADDON to open. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "opens the addon if only one matches" do api.post_addon('example', 'redistogo:nano') require("launchy") - Launchy.should_receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/redistogo:nano").and_return(Thread.new {}) + expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/redistogo:nano").and_return(Thread.new {}) stderr, stdout = execute('addons:open redistogo:nano') - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT Opening redistogo:nano for example... done STDOUT end @@ -577,39 +577,39 @@ module Heroku::Command } ) stderr, stdout = execute('addons:open deployhooks') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Ambiguous addon name: deployhooks ! Perhaps you meant `deployhooks:email` or `deployhooks:http`. STDERR - stdout.should == '' + expect(stdout).to eq('') Excon.stubs.shift end it "complains if no such addon exists" do stderr, stdout = execute('addons:open unknown') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! `unknown` is not a heroku add-on. ! See `heroku addons:list` for all available addons. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "suggests alternatives if addon has typo" do stderr, stdout = execute('addons:open redisgoto') - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! `redisgoto` is not a heroku add-on. ! Perhaps you meant `redistogo`. ! See `heroku addons:list` for all available addons. STDERR - stdout.should == '' + expect(stdout).to eq('') end it "complains if addon is not installed" do stderr, stdout = execute('addons:open deployhooks:http') - stderr.should == <<-STDOUT + expect(stderr).to eq <<-STDOUT ! Addon not installed: deployhooks:http STDOUT - stdout.should == '' + expect(stdout).to eq('') end end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index ac8f5689e..b598f4679 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -21,8 +21,8 @@ module Heroku::Command it "displays impicit app info" do stderr, stdout = execute("apps:info") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Git URL: git@heroku.com:example.git Owner Email: email@example.com @@ -33,8 +33,8 @@ module Heroku::Command it "gets explicit app from --app" do stderr, stdout = execute("apps:info --app example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Git URL: git@heroku.com:example.git Owner Email: email@example.com @@ -45,8 +45,8 @@ module Heroku::Command it "shows shell app info when --shell option is used" do stderr, stdout = execute("apps:info --shell") - stderr.should == "" - stdout.should match Regexp.new(<<-STDOUT) + expect(stderr).to eq("") + expect(stdout).to match Regexp.new(<<-STDOUT) create_status=complete created_at=\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4} dynos=0 @@ -73,8 +73,8 @@ module Heroku::Command with_blank_git_repository do stderr, stdout = execute("apps:create") name = api.get_apps.body.first["name"] - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating #{name}... done, stack is bamboo-mri-1.9.2 http://#{name}.herokuapp.com/ | git@heroku.com:#{name}.git Git remote heroku added @@ -86,8 +86,8 @@ module Heroku::Command it "with a name" do with_blank_git_repository do stderr, stdout = execute("apps:create example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 http://example.herokuapp.com/ | git@heroku.com:example.git Git remote heroku added @@ -99,8 +99,8 @@ module Heroku::Command it "with -a name" do with_blank_git_repository do stderr, stdout = execute("apps:create -a example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 http://example.herokuapp.com/ | git@heroku.com:example.git Git remote heroku added @@ -112,8 +112,8 @@ module Heroku::Command it "with --no-remote" do with_blank_git_repository do stderr, stdout = execute("apps:create example --no-remote") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 http://example.herokuapp.com/ | git@heroku.com:example.git STDOUT @@ -124,8 +124,8 @@ module Heroku::Command it "with addons" do with_blank_git_repository do stderr, stdout = execute("apps:create addonapp --addon custom_domains:basic,releases:basic") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating addonapp... done, stack is bamboo-mri-1.9.2 Adding custom_domains:basic to addonapp... done Adding releases:basic to addonapp... done @@ -139,8 +139,8 @@ module Heroku::Command it "with a buildpack" do with_blank_git_repository do stderr, stdout = execute("apps:create buildpackapp --buildpack http://example.org/buildpack.git") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating buildpackapp... done, stack is bamboo-mri-1.9.2 BUILDPACK_URL=http://example.org/buildpack.git http://buildpackapp.herokuapp.com/ | git@heroku.com:buildpackapp.git @@ -153,8 +153,8 @@ module Heroku::Command it "with an alternate remote name" do with_blank_git_repository do stderr, stdout = execute("apps:create alternate-remote --remote alternate") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating alternate-remote... done, stack is bamboo-mri-1.9.2 http://alternate-remote.herokuapp.com/ | git@heroku.com:alternate-remote.git Git remote alternate added @@ -178,8 +178,8 @@ module Heroku::Command it "succeeds" do stub_core.list.returns([["example", "user"]]) stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === My Apps example @@ -203,8 +203,8 @@ module Heroku::Command it "displays a message when the org has no apps" do Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { :status => 200, :body => Heroku::OkJson.encode([]) }) stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT There are no apps in organization test-org. STDOUT @@ -225,8 +225,8 @@ module Heroku::Command it "lists joined apps in an organization" do stderr, stdout = execute("apps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Apps joined in organization test-org org-app-1 @@ -235,8 +235,8 @@ module Heroku::Command it "list all apps in an organization with the --all flag" do stderr, stdout = execute("apps --all") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Apps joined in organization test-org org-app-1 @@ -264,8 +264,8 @@ module Heroku::Command it "renames app" do with_blank_git_repository do stderr, stdout = execute("apps:rename example2") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Renaming example to example2... done http://example2.herokuapp.com/ | git@heroku.com:example2.git Don't forget to update your Git remotes on any local checkouts. @@ -277,11 +277,11 @@ module Heroku::Command it "displays an error if no name is specified" do stderr, stdout = execute("apps:rename") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku apps:rename NEWNAME ! Must specify NEWNAME to rename. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -294,8 +294,8 @@ module Heroku::Command it "succeeds with app explicitly specified with --app and user confirmation" do stderr, stdout = execute("apps:destroy --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Destroying example (including all add-ons)... done STDOUT end @@ -308,25 +308,25 @@ module Heroku::Command it "fails with explicit app but no confirmation" do stderr, stdout = execute("apps:destroy example") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Confirmation did not match example. Aborted. STDERR - stdout.should == " + expect(stdout).to eq(" ! WARNING: Potentially Destructive Action ! This command will destroy example (including all add-ons). ! To proceed, type \"example\" or re-run this command with --confirm example -> " +> ") end it "fails without explicit app" do stderr, stdout = execute("apps:destroy") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku apps:destroy --app APP ! Must specify APP to destroy. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -338,13 +338,13 @@ module Heroku::Command it "creates adding heroku to git remote" do with_blank_git_repository do stderr, stdout = execute("apps:create example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 http://example.herokuapp.com/ | git@heroku.com:example.git Git remote heroku added STDOUT - `git remote`.strip.should match(/^heroku$/) + expect(`git remote`.strip).to match(/^heroku$/) api.delete_app("example") end end @@ -352,13 +352,13 @@ module Heroku::Command it "creates adding a custom git remote" do with_blank_git_repository do stderr, stdout = execute("apps:create example --remote myremote") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 http://example.herokuapp.com/ | git@heroku.com:example.git Git remote myremote added STDOUT - `git remote`.strip.should match(/^myremote$/) + expect(`git remote`.strip).to match(/^myremote$/) api.delete_app("example") end end @@ -367,8 +367,8 @@ module Heroku::Command with_blank_git_repository do `git remote add heroku /tmp/git_spec_#{Process.pid}` stderr, stdout = execute("apps:create example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 http://example.herokuapp.com/ | git@heroku.com:example.git STDOUT @@ -387,7 +387,7 @@ module Heroku::Command api.delete_app("example2") remotes = `git remote -v` - remotes.should == <<-REMOTES + expect(remotes).to eq <<-REMOTES github\tgit@github.com:test/test.git (fetch) github\tgit@github.com:test/test.git (push) production\tgit@heroku.com:example2.git (fetch) @@ -405,7 +405,7 @@ module Heroku::Command api.post_app("name" => "example", "stack" => "cedar") stderr, stdout = execute("apps:destroy --confirm example") - `git remote`.strip.should_not include('heroku') + expect(`git remote`.strip).not_to include('heroku') end end end diff --git a/spec/heroku/command/auth_spec.rb b/spec/heroku/command/auth_spec.rb index c58eb0412..b5b981808 100644 --- a/spec/heroku/command/auth_spec.rb +++ b/spec/heroku/command/auth_spec.rb @@ -6,10 +6,10 @@ it "displays heroku help auth" do stderr, stdout = execute("auth") - stderr.should == "" - stdout.should include "Additional commands" - stdout.should include "auth:login" - stdout.should include "auth:logout" + expect(stderr).to eq("") + expect(stdout).to include "Additional commands" + expect(stdout).to include "auth:login" + expect(stdout).to include "auth:logout" end end @@ -17,8 +17,8 @@ it "displays the user's api key" do stderr, stdout = execute("auth:token") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT apikey01 STDOUT end @@ -27,8 +27,8 @@ describe "auth:whoami" do it "displays the user's email address" do stderr, stdout = execute("auth:whoami") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT email@example.com STDOUT end diff --git a/spec/heroku/command/base_spec.rb b/spec/heroku/command/base_spec.rb index 1fc7f31c3..8b1cfec20 100644 --- a/spec/heroku/command/base_spec.rb +++ b/spec/heroku/command/base_spec.rb @@ -5,37 +5,37 @@ module Heroku::Command describe Base do before do @base = Base.new - @base.stub(:display) + allow(@base).to receive(:display) @client = double('heroku client', :host => 'heroku.com') end describe "confirming" do it "confirms the app via --confirm" do - Heroku::Command.stub(:current_options).and_return(:confirm => "example") - @base.stub(:app).and_return("example") - @base.confirm_command.should be_truthy + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "example") + allow(@base).to receive(:app).and_return("example") + expect(@base.confirm_command).to be_truthy end it "does not confirms the app via --confirm on a mismatch" do - Heroku::Command.stub(:current_options).and_return(:confirm => "badapp") - @base.stub(:app).and_return("example") - lambda { @base.confirm_command}.should raise_error CommandFailed + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "badapp") + allow(@base).to receive(:app).and_return("example") + expect { @base.confirm_command}.to raise_error CommandFailed end it "confirms the app interactively via ask" do - @base.stub(:app).and_return("example") - @base.stub(:ask).and_return("example") - Heroku::Command.stub(:current_options).and_return({}) - @base.confirm_command.should be_truthy + allow(@base).to receive(:app).and_return("example") + allow(@base).to receive(:ask).and_return("example") + allow(Heroku::Command).to receive(:current_options).and_return({}) + expect(@base.confirm_command).to be_truthy end it "fails if the interactive confirm doesn't match" do - @base.stub(:app).and_return("example") - @base.stub(:ask).and_return("badresponse") - Heroku::Command.stub(:current_options).and_return({}) - capture_stderr do - lambda { @base.confirm_command }.should raise_error(SystemExit) - end.should == <<-STDERR + allow(@base).to receive(:app).and_return("example") + allow(@base).to receive(:ask).and_return("badresponse") + allow(Heroku::Command).to receive(:current_options).and_return({}) + expect(capture_stderr do + expect { @base.confirm_command }.to raise_error(SystemExit) + end).to eq <<-STDERR ! Confirmation did not match example. Aborted. STDERR end @@ -43,34 +43,34 @@ module Heroku::Command context "detecting the app" do it "attempts to find the app via the --app option" do - @base.stub(:options).and_return(:app => "example") - @base.app.should == "example" + allow(@base).to receive(:options).and_return(:app => "example") + expect(@base.app).to eq("example") end it "attempts to find the app via the --confirm option" do - @base.stub(:options).and_return(:confirm => "myconfirmapp") - @base.app.should == "myconfirmapp" + allow(@base).to receive(:options).and_return(:confirm => "myconfirmapp") + expect(@base.app).to eq("myconfirmapp") end it "attempts to find the app via HEROKU_APP when not explicitly specified" do ENV['HEROKU_APP'] = "myenvapp" - @base.app.should == "myenvapp" - @base.stub(:options).and_return([]) - @base.app.should == "myenvapp" + expect(@base.app).to eq("myenvapp") + allow(@base).to receive(:options).and_return([]) + expect(@base.app).to eq("myenvapp") ENV.delete('HEROKU_APP') end it "overrides HEROKU_APP when explicitly specified" do ENV['HEROKU_APP'] = "myenvapp" - @base.stub(:options).and_return(:app => "example") - @base.app.should == "example" + allow(@base).to receive(:options).and_return(:app => "example") + expect(@base.app).to eq("example") ENV.delete('HEROKU_APP') end it "read remotes from git config" do - Dir.stub(:chdir) - File.should_receive(:exists?).with(".git").and_return(true) - @base.should_receive(:git).with('remote -v').and_return(<<-REMOTES) + allow(Dir).to receive(:chdir) + expect(File).to receive(:exists?).with(".git").and_return(true) + expect(@base).to receive(:git).with('remote -v').and_return(<<-REMOTES) staging\tgit@heroku.com:example-staging.git (fetch) staging\tgit@heroku.com:example-staging.git (push) production\tgit@heroku.com:example.git (fetch) @@ -80,28 +80,28 @@ module Heroku::Command REMOTES @heroku = double - @heroku.stub(:host).and_return('heroku.com') - @base.stub(:heroku).and_return(@heroku) + allow(@heroku).to receive(:host).and_return('heroku.com') + allow(@base).to receive(:heroku).and_return(@heroku) # need a better way to test internal functionality - @base.send(:git_remotes, '/home/dev/example').should == { 'staging' => 'example-staging', 'production' => 'example' } + expect(@base.send(:git_remotes, '/home/dev/example')).to eq({ 'staging' => 'example-staging', 'production' => 'example' }) end it "gets the app from remotes when there's only one app" do - @base.stub(:git_remotes).and_return({ 'heroku' => 'example' }) - @base.stub(:git).with("config heroku.remote").and_return("") - @base.app.should == 'example' + allow(@base).to receive(:git_remotes).and_return({ 'heroku' => 'example' }) + allow(@base).to receive(:git).with("config heroku.remote").and_return("") + expect(@base.app).to eq('example') end it "accepts a --remote argument to choose the app from the remote name" do - @base.stub(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) - @base.stub(:options).and_return(:remote => "staging") - @base.app.should == 'example-staging' + allow(@base).to receive(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) + allow(@base).to receive(:options).and_return(:remote => "staging") + expect(@base.app).to eq('example-staging') end it "raises when cannot determine which app is it" do - @base.stub(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) - lambda { @base.app }.should raise_error(Heroku::Command::CommandFailed) + allow(@base).to receive(:git_remotes).and_return({ 'staging' => 'example-staging', 'production' => 'example' }) + expect { @base.app }.to raise_error(Heroku::Command::CommandFailed) end end diff --git a/spec/heroku/command/certs_spec.rb b/spec/heroku/command/certs_spec.rb index e1801f52b..0c26d50ff 100644 --- a/spec/heroku/command/certs_spec.rb +++ b/spec/heroku/command/certs_spec.rb @@ -43,7 +43,7 @@ module Heroku::Command it "shows a list of certs" do stub_core.ssl_endpoint_list("example").returns([endpoint, endpoint2]) stderr, stdout = execute("certs") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Endpoint Common Name(s) Expires Trusted ------------------------ -------------- -------------------- ------- tokyo-1050.herokussl.com example.org 2013-08-01 21:34 UTC False @@ -54,7 +54,7 @@ module Heroku::Command it "warns about no SSL Endpoints if the app has no certs" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT example has no SSL Endpoints. Use `heroku certs:add CRT KEY` to add one. STDOUT @@ -63,12 +63,12 @@ module Heroku::Command describe "certs:add" do it "adds an endpoint" do - File.should_receive(:read).with("pem_file").and_return("pem content") - File.should_receive(:read).with("key_file").and_return("key content") + expect(File).to receive(:read).with("pem_file").and_return("pem content") + expect(File).to receive(:read).with("key_file").and_return("key content") stub_core.ssl_endpoint_add('example', 'pem content', 'key content').returns(endpoint) stderr, stdout = execute("certs:add --bypass pem_file key_file") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Adding SSL Endpoint to example... done example now served by tokyo-1050.herokussl.com Certificate details: @@ -77,7 +77,7 @@ module Heroku::Command end it "shows usage if two arguments are not provided" do - lambda { execute("certs:add --bypass") }.should raise_error(CommandFailed, /Usage:/) + expect { execute("certs:add --bypass") }.to raise_error(CommandFailed, /Usage:/) end end @@ -87,7 +87,7 @@ module Heroku::Command stub_core.ssl_endpoint_info('example', 'tokyo-1050.herokussl.com').returns(endpoint) stderr, stdout = execute("certs:info") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Fetching SSL Endpoint tokyo-1050.herokussl.com info for example... done Certificate details: #{certificate_details} @@ -98,7 +98,7 @@ module Heroku::Command stub_core.ssl_endpoint_info('example', 'tokyo-1050').returns(endpoint) stderr, stdout = execute("certs:info --endpoint tokyo-1050") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Fetching SSL Endpoint tokyo-1050 info for example... done Certificate details: #{certificate_details} @@ -109,7 +109,7 @@ module Heroku::Command stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:info") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end @@ -121,31 +121,31 @@ module Heroku::Command stub_core.ssl_endpoint_remove('example', 'tokyo-1050.herokussl.com').returns(endpoint) stderr, stdout = execute("certs:remove --confirm example") - stdout.should include "Removing SSL Endpoint tokyo-1050.herokussl.com from example..." - stdout.should include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." + expect(stdout).to include "Removing SSL Endpoint tokyo-1050.herokussl.com from example..." + expect(stdout).to include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." end it "allows an endpoint to be specified" do stub_core.ssl_endpoint_remove('example', 'tokyo-1050').returns(endpoint) stderr, stdout = execute("certs:remove --confirm example --endpoint tokyo-1050") - stdout.should include "Removing SSL Endpoint tokyo-1050 from example..." - stdout.should include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." + expect(stdout).to include "Removing SSL Endpoint tokyo-1050 from example..." + expect(stdout).to include "NOTE: Billing is still active. Remove SSL Endpoint add-on to stop billing." end it "requires confirmation" do stub_core.ssl_endpoint_list("example").returns([endpoint]) stderr, stdout = execute("certs:remove") - stdout.should include "WARNING: Potentially Destructive Action" - stdout.should include "This command will remove the endpoint tokyo-1050.herokussl.com from example." + expect(stdout).to include "WARNING: Potentially Destructive Action" + expect(stdout).to include "This command will remove the endpoint tokyo-1050.herokussl.com from example." end it "shows an error if an app has no endpoints" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:remove") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end @@ -153,8 +153,8 @@ module Heroku::Command describe "certs:update" do before do - File.should_receive(:read).with("pem_file").and_return("pem content") - File.should_receive(:read).with("key_file").and_return("key content") + expect(File).to receive(:read).with("pem_file").and_return("pem content") + expect(File).to receive(:read).with("key_file").and_return("key content") end it "updates an endpoint" do @@ -162,7 +162,7 @@ module Heroku::Command stub_core.ssl_endpoint_update('example', 'tokyo-1050.herokussl.com', 'pem content', 'key content').returns(endpoint) stderr, stdout = execute("certs:update --confirm example --bypass pem_file key_file") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Updating SSL Endpoint tokyo-1050.herokussl.com for example... done Updated certificate details: #{certificate_details} @@ -173,7 +173,7 @@ module Heroku::Command stub_core.ssl_endpoint_update('example', 'tokyo-1050', 'pem content', 'key content').returns(endpoint) stderr, stdout = execute("certs:update --confirm example --bypass --endpoint tokyo-1050 pem_file key_file") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Updating SSL Endpoint tokyo-1050 for example... done Updated certificate details: #{certificate_details} @@ -184,15 +184,15 @@ module Heroku::Command stub_core.ssl_endpoint_list("example").returns([endpoint]) stderr, stdout = execute("certs:update --bypass pem_file key_file") - stdout.should include "WARNING: Potentially Destructive Action" - stdout.should include "This command will change the certificate of endpoint tokyo-1050.herokussl.com on example." + expect(stdout).to include "WARNING: Potentially Destructive Action" + expect(stdout).to include "This command will change the certificate of endpoint tokyo-1050.herokussl.com on example." end it "shows an error if an app has no endpoints" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:update --bypass pem_file key_file") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end @@ -204,7 +204,7 @@ module Heroku::Command stub_core.ssl_endpoint_rollback('example', 'tokyo-1050.herokussl.com').returns(endpoint) stderr, stdout = execute("certs:rollback --confirm example") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Rolling back SSL Endpoint tokyo-1050.herokussl.com for example... done New active certificate details: #{certificate_details} @@ -215,7 +215,7 @@ module Heroku::Command stub_core.ssl_endpoint_rollback('example', 'tokyo-1050').returns(endpoint) stderr, stdout = execute("certs:rollback --confirm example --endpoint tokyo-1050") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Rolling back SSL Endpoint tokyo-1050 for example... done New active certificate details: #{certificate_details} @@ -226,15 +226,15 @@ module Heroku::Command stub_core.ssl_endpoint_list("example").returns([endpoint]) stderr, stdout = execute("certs:rollback") - stdout.should include "WARNING: Potentially Destructive Action" - stdout.should include "This command will rollback the certificate of endpoint tokyo-1050.herokussl.com on example." + expect(stdout).to include "WARNING: Potentially Destructive Action" + expect(stdout).to include "This command will rollback the certificate of endpoint tokyo-1050.herokussl.com on example." end it "shows an error if an app has no endpoints" do stub_core.ssl_endpoint_list("example").returns([]) stderr, stdout = execute("certs:rollback") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! example has no SSL Endpoints. STDERR end diff --git a/spec/heroku/command/config_spec.rb b/spec/heroku/command/config_spec.rb index 8c30a2269..aaf816412 100644 --- a/spec/heroku/command/config_spec.rb +++ b/spec/heroku/command/config_spec.rb @@ -15,8 +15,8 @@ module Heroku::Command it "shows all configs" do api.put_config_vars("example", { 'FOO_BAR' => 'one', 'BAZ_QUX' => 'two' }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars BAZ_QUX: two FOO_BAR: one @@ -26,8 +26,8 @@ module Heroku::Command it "does not trim long values" do api.put_config_vars("example", { 'LONG' => 'A' * 60 }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars LONG: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA STDOUT @@ -36,8 +36,8 @@ module Heroku::Command it "handles when value is nil" do api.put_config_vars("example", { 'FOO_BAR' => 'one', 'BAZ_QUX' => nil }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars BAZ_QUX: FOO_BAR: one @@ -47,8 +47,8 @@ module Heroku::Command it "handles when value is a boolean" do api.put_config_vars("example", { 'FOO_BAR' => 'one', 'BAZ_QUX' => true }) stderr, stdout = execute("config") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Config Vars BAZ_QUX: true FOO_BAR: one @@ -58,8 +58,8 @@ module Heroku::Command it "shows configs in a shell compatible format" do api.put_config_vars("example", { 'A' => 'one', 'B' => 'two three' }) stderr, stdout = execute("config --shell") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT A=one B=two three STDOUT @@ -68,8 +68,8 @@ module Heroku::Command it "shows a single config for get" do api.put_config_vars("example", { 'LONG' => 'A' * 60 }) stderr, stdout = execute("config:get LONG") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA STDOUT end @@ -78,8 +78,8 @@ module Heroku::Command it "sets config vars" do stderr, stdout = execute("config:set A=1 B=2") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting config vars and restarting example... done, v1 A: 1 B: 2 @@ -88,8 +88,8 @@ module Heroku::Command it "allows config vars with = in the value" do stderr, stdout = execute("config:set A=b=c") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting config vars and restarting example... done, v1 A: b=c STDOUT @@ -97,8 +97,8 @@ module Heroku::Command it "sets config vars without changing case" do stderr, stdout = execute("config:set a=b") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting config vars and restarting example... done, v1 a: b STDOUT @@ -110,19 +110,19 @@ module Heroku::Command it "exits with a help notice when no keys are provides" do stderr, stdout = execute("config:unset") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku config:unset KEY1 [KEY2 ...] ! Must specify KEY to unset. STDERR - stdout.should == "" + expect(stdout).to eq("") end context "when one key is provided" do it "unsets a single key" do stderr, stdout = execute("config:unset A") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Unsetting A and restarting example... done, v1 STDOUT end @@ -132,8 +132,8 @@ module Heroku::Command it "unsets all given keys" do stderr, stdout = execute("config:unset A B") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Unsetting A and restarting example... done, v1 Unsetting B and restarting example... done, v2 STDOUT diff --git a/spec/heroku/command/domains_spec.rb b/spec/heroku/command/domains_spec.rb index 2b2432d66..c98cd31ea 100644 --- a/spec/heroku/command/domains_spec.rb +++ b/spec/heroku/command/domains_spec.rb @@ -21,8 +21,8 @@ module Heroku::Command it "lists message with no domains" do stderr, stdout = execute("domains") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT example has no domain names. STDOUT end @@ -30,8 +30,8 @@ module Heroku::Command it "lists domains when some exist" do api.post_domain("example", "example.com") stderr, stdout = execute("domains") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Domain Names example.com @@ -43,8 +43,8 @@ module Heroku::Command it "adds domain names" do stderr, stdout = execute("domains:add example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Adding example.com to example... done STDOUT api.delete_domain("example", "example.com") @@ -52,7 +52,7 @@ module Heroku::Command it "shows usage if no domain specified for add" do stderr, stdout = execute("domains:add") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku domains:add DOMAIN ! Must specify DOMAIN to add. STDERR @@ -61,15 +61,15 @@ module Heroku::Command it "removes domain names" do api.post_domain("example", "example.com") stderr, stdout = execute("domains:remove example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing example.com from example... done STDOUT end it "shows usage if no domain specified for remove" do stderr, stdout = execute("domains:remove") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku domains:remove DOMAIN ! Must specify DOMAIN to remove. STDERR @@ -78,8 +78,8 @@ module Heroku::Command it "removes all domain names" do stub_core.remove_domains("example") stderr, stdout = execute("domains:clear") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing all domain names from example... done STDOUT end diff --git a/spec/heroku/command/drains_spec.rb b/spec/heroku/command/drains_spec.rb index 52edddc56..629f88ce8 100644 --- a/spec/heroku/command/drains_spec.rb +++ b/spec/heroku/command/drains_spec.rb @@ -7,8 +7,8 @@ it "can list drains" do stub_core.list_drains("example").returns("drains") stderr, stdout = execute("drains") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT drains STDOUT end @@ -16,8 +16,8 @@ it "can add drains" do stub_core.add_drain("example", "syslog://localhost/add").returns("added") stderr, stdout = execute("drains:add syslog://localhost/add") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT added STDOUT end @@ -25,8 +25,8 @@ it "can remove drains" do stub_core.remove_drain("example", "syslog://localhost/remove").returns("removed") stderr, stdout = execute("drains:remove syslog://localhost/remove") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT removed STDOUT end diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb index af55bb6eb..85486faf6 100644 --- a/spec/heroku/command/fork_spec.rb +++ b/spec/heroku/command/fork_spec.rb @@ -39,8 +39,8 @@ module Heroku::Command it "forks an app" do stderr, stdout = execute("fork example-fork") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating fork example-fork... done Copying slug... done Copying config vars... done @@ -49,8 +49,8 @@ module Heroku::Command end it "copies slug" do - Heroku::API.any_instance.should_receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original - Heroku::API.any_instance.should_receive(:post_release_v3).with("example-fork", "SLUG_ID", "Forked from example").and_call_original + expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original + expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", "SLUG_ID", "Forked from example").and_call_original execute("fork example-fork") end @@ -62,14 +62,14 @@ module Heroku::Command } api.put_config_vars("example", config_vars) execute("fork example-fork") - api.get_config_vars("example-fork").body.should == config_vars + expect(api.get_config_vars("example-fork").body).to eq(config_vars) end it "re-provisions add-ons" do addons = ["pgbackups:basic", "deployhooks:http"].sort addons.each { |a| api.post_addon("example", a) } execute("fork example-fork") - api.get_addons("example-fork").body.collect { |info| info["name"] }.sort.should == addons + expect(api.get_addons("example-fork").body.collect { |info| info["name"] }.sort).to eq(addons) end end @@ -83,7 +83,7 @@ module Heroku::Command execute("fork example-fork") raise rescue Heroku::Command::CommandFailed => e - e.message.should == "No releases on example" + expect(e.message).to eq("No releases on example") ensure Excon.stubs.shift end @@ -98,16 +98,16 @@ module Heroku::Command execute("fork example-fork") raise rescue Heroku::Command::CommandFailed => e - e.message.should == "No slug on example" + expect(e.message).to eq("No slug on example") ensure Excon.stubs.shift end end it "doesn't attempt to fork to the same app" do - lambda do + expect do execute("fork example") - end.should raise_error(Heroku::Command::CommandFailed, /same app/) + end.to raise_error(Heroku::Command::CommandFailed, /same app/) end it "confirms before deleting the app" do @@ -118,13 +118,14 @@ module Heroku::Command ensure Excon.stubs.shift end - api.get_apps.body.map { |app| app["name"] }.should == + expect(api.get_apps.body.map { |app| app["name"] }).to eq( %w( example example-fork ) + ) end it "deletes fork app on error, before re-raising" do stub(Heroku::Command).confirm_command.returns(true) - api.get_apps.body.map { |app| app["name"] }.should == %w( example ) + expect(api.get_apps.body.map { |app| app["name"] }).to eq(%w( example )) end end end diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb index b878fad29..b57adc4e0 100644 --- a/spec/heroku/command/git_spec.rb +++ b/spec/heroku/command/git_spec.rb @@ -25,8 +25,8 @@ module Heroku::Command end end stderr, stdout = execute("git:clone example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Cloning from app 'example'... Cloning into 'example'... STDOUT @@ -39,8 +39,8 @@ module Heroku::Command end end stderr, stdout = execute("git:clone example somedir") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Cloning from app 'example'... Cloning into 'somedir'... STDOUT @@ -53,8 +53,8 @@ module Heroku::Command end end stderr, stdout = execute("git:clone -a example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Cloning from app 'example'... Cloning into 'example'... STDOUT @@ -67,8 +67,8 @@ module Heroku::Command end end stderr, stdout = execute("git:clone -a example somedir") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Cloning from app 'example'... Cloning into 'somedir'... STDOUT @@ -81,8 +81,8 @@ module Heroku::Command end end stderr, stdout = execute("git:clone example -r other") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Cloning from app 'example'... Cloning into 'example'... STDOUT @@ -109,8 +109,8 @@ module Heroku::Command stub(git).git('remote add heroku git@heroku.com:example.git') end stderr, stdout = execute("git:remote") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Git remote heroku added STDOUT end @@ -121,8 +121,8 @@ module Heroku::Command stub(git).git('remote add other git@heroku.com:example.git') end stderr, stdout = execute("git:remote -r other") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Git remote other added STDOUT end @@ -132,10 +132,10 @@ module Heroku::Command stub(git).git('remote').returns("heroku") end stderr, stdout = execute("git:remote") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Git remote heroku already exists STDERR - stdout.should == "" + expect(stdout).to eq("") end end diff --git a/spec/heroku/command/help_spec.rb b/spec/heroku/command/help_spec.rb index e851cdca5..5f1918a93 100644 --- a/spec/heroku/command/help_spec.rb +++ b/spec/heroku/command/help_spec.rb @@ -7,64 +7,64 @@ describe "help" do it "should show root help with no args" do stderr, stdout = execute("help") - stderr.should == "" - stdout.should include "Usage: heroku COMMAND [--app APP] [command-specific-options]" - stdout.should include "apps" - stdout.should include "help" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku COMMAND [--app APP] [command-specific-options]" + expect(stdout).to include "apps" + expect(stdout).to include "help" end it "should show command help and namespace help when ambigious" do stderr, stdout = execute("help apps") - stderr.should == "" - stdout.should include "heroku apps" - stdout.should include "list your apps" - stdout.should include "Additional commands" - stdout.should include "apps:create" + expect(stderr).to eq("") + expect(stdout).to include "heroku apps" + expect(stdout).to include "list your apps" + expect(stdout).to include "Additional commands" + expect(stdout).to include "apps:create" end it "should show only command help when not ambiguous" do stderr, stdout = execute("help apps:create") - stderr.should == "" - stdout.should include "heroku apps:create" - stdout.should include "create a new app" - stdout.should_not include "Additional commands" + expect(stderr).to eq("") + expect(stdout).to include "heroku apps:create" + expect(stdout).to include "create a new app" + expect(stdout).not_to include "Additional commands" end it "should show command help with --help" do stderr, stdout = execute("apps:create --help") - stderr.should == "" - stdout.should include "Usage: heroku apps:create" - stdout.should include "create a new app" - stdout.should_not include "Additional commands" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku apps:create" + expect(stdout).to include "create a new app" + expect(stdout).not_to include "Additional commands" end it "should redirect if the command is an alias" do stderr, stdout = execute("help create") - stderr.should == "" - stdout.should include "Alias: create redirects to apps:create" - stdout.should include "Usage: heroku apps:create" - stdout.should include "create a new app" - stdout.should_not include "Additional commands" + expect(stderr).to eq("") + expect(stdout).to include "Alias: create redirects to apps:create" + expect(stdout).to include "Usage: heroku apps:create" + expect(stdout).to include "create a new app" + expect(stdout).not_to include "Additional commands" end it "should show if the command does not exist" do stderr, stdout = execute("help sudo:sandwich") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! sudo:sandwich is not a heroku command. See `heroku help`. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "should show help with naked -h" do stderr, stdout = execute("-h") - stderr.should == "" - stdout.should include "Usage: heroku COMMAND" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku COMMAND" end it "should show help with naked --help" do stderr, stdout = execute("--help") - stderr.should == "" - stdout.should include "Usage: heroku COMMAND" + expect(stderr).to eq("") + expect(stdout).to include "Usage: heroku COMMAND" end describe "with legacy help" do @@ -72,21 +72,21 @@ it "displays the legacy group in the namespace list" do stderr, stdout = execute("help") - stderr.should == "" - stdout.should include "Foo Group" + expect(stderr).to eq("") + expect(stdout).to include "Foo Group" end it "displays group help" do stderr, stdout = execute("help foo") - stderr.should == "" - stdout.should include "do a bar to foo" - stdout.should include "do a baz to foo" + expect(stderr).to eq("") + expect(stdout).to include "do a bar to foo" + expect(stdout).to include "do a baz to foo" end it "displays legacy command-specific help" do stderr, stdout = execute("help foo:bar") - stderr.should == "" - stdout.should include "do a bar to foo" + expect(stderr).to eq("") + expect(stdout).to include "do a bar to foo" end end end diff --git a/spec/heroku/command/keys_spec.rb b/spec/heroku/command/keys_spec.rb index cfc18737e..d479f17ab 100644 --- a/spec/heroku/command/keys_spec.rb +++ b/spec/heroku/command/keys_spec.rb @@ -16,14 +16,14 @@ module Heroku::Command end it "tries to find a key if no key filename is supplied" do - Heroku::Auth.should_receive(:ask).and_return("y") - Heroku::Auth.should_receive(:generate_ssh_key) - File.should_receive(:exists?).with('.git').and_return(false) - File.should_receive(:exists?).with('/.ssh/id_rsa.pub').and_return(true) - File.should_receive(:read).with('/.ssh/id_rsa.pub').and_return(KEY) + expect(Heroku::Auth).to receive(:ask).and_return("y") + expect(Heroku::Auth).to receive(:generate_ssh_key) + expect(File).to receive(:exists?).with('.git').and_return(false) + expect(File).to receive(:exists?).with('/.ssh/id_rsa.pub').and_return(true) + expect(File).to receive(:read).with('/.ssh/id_rsa.pub').and_return(KEY) stderr, stdout = execute("keys:add") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Could not find an existing public key. Would you like to generate one? [Yn] Generating new SSH public key. Uploading SSH public key /.ssh/id_rsa.pub... done @@ -31,12 +31,12 @@ module Heroku::Command end it "adds a key from a specified keyfile path" do - File.should_receive(:exists?).with('.git').and_return(false) - File.should_receive(:exists?).with('/my/key.pub').and_return(true) - File.should_receive(:read).with('/my/key.pub').and_return(KEY) + expect(File).to receive(:exists?).with('.git').and_return(false) + expect(File).to receive(:exists?).with('/my/key.pub').and_return(true) + expect(File).to receive(:read).with('/my/key.pub').and_return(KEY) stderr, stdout = execute("keys:add /my/key.pub") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Uploading SSH public key /my/key.pub... done STDOUT end @@ -55,8 +55,8 @@ module Heroku::Command it "list keys, trimming the hex code for better display" do stderr, stdout = execute("keys") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === email@example.com Keys ssh-rsa AAAAB3NzaC...Fyoke4MQ== pedro@heroku @@ -65,8 +65,8 @@ module Heroku::Command it "list keys showing the whole key hex with --long" do stderr, stdout = execute("keys --long") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === email@example.com Keys ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAp9AJD5QABmOcrkHm6SINuQkDefaR0MUrfgZ1Pxir3a4fM1fwa00dsUwbUaRuR7FEFD8n1E9WwDf8SwQTHtyZsJg09G9myNqUzkYXCmydN7oGr5IdVhRyv5ixcdiE0hj7dRnOJg2poSQ3Qi+Ka8SVJzF7nIw1YhuicHPSbNIFKi5s0D5a+nZb/E6MNGvhxoFCQX2IcNxaJMqhzy1ESwlixz45aT72mXYq0LIxTTpoTqma1HuKdRY8HxoREiivjmMQulYP+CxXFcMyV9kxTKIUZ/FXqlC6G5vSm3J4YScSatPOj9ID5HowpdlIx8F6y4p1/28r2tTl4CY40FFyoke4MQ== pedro@heroku @@ -85,8 +85,8 @@ module Heroku::Command it "succeeds" do stderr, stdout = execute("keys:remove pedro@heroku") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing pedro@heroku SSH key... done STDOUT end @@ -95,11 +95,11 @@ module Heroku::Command it "displays an error if no key is specified" do stderr, stdout = execute("keys:remove") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku keys:remove KEY ! Must specify KEY to remove. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -108,8 +108,8 @@ module Heroku::Command it "succeeds" do stderr, stdout = execute("keys:clear") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing all SSH keys... done STDOUT end diff --git a/spec/heroku/command/labs_spec.rb b/spec/heroku/command/labs_spec.rb index 60c935074..a0848ece2 100644 --- a/spec/heroku/command/labs_spec.rb +++ b/spec/heroku/command/labs_spec.rb @@ -16,8 +16,8 @@ module Heroku::Command it "lists available features" do stderr, stdout = execute("labs:list") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === User Features (email@example.com) [ ] sumo-rankings Heroku Sumo ranks and visualizes the scale of your app, and suggests the optimum combination of dynos and add-ons to take it to the next level. @@ -30,8 +30,8 @@ module Heroku::Command it "lists enabled features" do stub_core.list_features("example").returns([]) stderr, stdout = execute("labs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === User Features (email@example.com) [ ] sumo-rankings Heroku Sumo ranks and visualizes the scale of your app, and suggests the optimum combination of dynos and add-ons to take it to the next level. @@ -43,8 +43,8 @@ module Heroku::Command it "displays details of a feature" do stderr, stdout = execute("labs:info user_env_compile") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === user_env_compile Docs: http://devcenter.heroku.com/articles/labs-user-env-compile Summary: Add user config vars to the environment during slug compilation @@ -53,17 +53,17 @@ module Heroku::Command it "shows usage if no feature name is specified for info" do stderr, stdout = execute("labs:info") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku labs:info FEATURE ! Must specify FEATURE for info. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "enables a feature" do stderr, stdout = execute("labs:enable user_env_compile") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Enabling user_env_compile for example... done WARNING: This feature is experimental and may change or be removed without notice. For more information see: http://devcenter.heroku.com/articles/labs-user-env-compile @@ -72,29 +72,29 @@ module Heroku::Command it "shows usage if no feature name is specified for enable" do stderr, stdout = execute("labs:enable") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku labs:enable FEATURE ! Must specify FEATURE to enable. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "disables a feature" do api.post_feature('user_env_compile', 'example') stderr, stdout = execute("labs:disable user_env_compile") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Disabling user_env_compile for example... done STDOUT end it "shows usage if no feature name is specified for disable" do stderr, stdout = execute("labs:disable") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku labs:disable FEATURE ! Must specify FEATURE to disable. STDERR - stdout.should == "" + expect(stdout).to eq("") end end end diff --git a/spec/heroku/command/logs_spec.rb b/spec/heroku/command/logs_spec.rb index edf15f541..9e69ab5c2 100644 --- a/spec/heroku/command/logs_spec.rb +++ b/spec/heroku/command/logs_spec.rb @@ -27,8 +27,8 @@ old_stdout_isatty = $stdout.isatty stub($stdout).isatty.returns(true) stderr, stdout = execute("logs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT \e[36m2011-01-01T00:00:00+00:00 app[web.1]:\e[0m test STDOUT stub($stdout).isatty.returns(old_stdout_isatty) @@ -38,8 +38,8 @@ old_stdout_isatty = $stdout.isatty stub($stdout).isatty.returns(false) stderr, stdout = execute("logs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT 2011-01-01T00:00:00+00:00 app[web.1]: test STDOUT stub($stdout).isatty.returns(old_stdout_isatty) @@ -48,8 +48,8 @@ it "does not use ansi if TERM is not set" do term = ENV.delete("TERM") stderr, stdout = execute("logs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT 2011-01-01T00:00:00+00:00 app[web.1]: test STDOUT ENV["TERM"] = term diff --git a/spec/heroku/command/maintenance_spec.rb b/spec/heroku/command/maintenance_spec.rb index 77168e91a..8eea6967d 100644 --- a/spec/heroku/command/maintenance_spec.rb +++ b/spec/heroku/command/maintenance_spec.rb @@ -15,8 +15,8 @@ module Heroku::Command it "displays off for maintenance mode of an app" do stderr, stdout = execute("maintenance") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT off STDOUT end @@ -25,24 +25,24 @@ module Heroku::Command api.post_app_maintenance('example', '1') stderr, stdout = execute("maintenance") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT on STDOUT end it "turns on maintenance mode for the app" do stderr, stdout = execute("maintenance:on") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Enabling maintenance mode for example... done STDOUT end it "turns off maintenance mode for the app" do stderr, stdout = execute("maintenance:off") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Disabling maintenance mode for example... done STDOUT end diff --git a/spec/heroku/command/orgs_spec.rb b/spec/heroku/command/orgs_spec.rb index 11eafecc8..7f60db918 100644 --- a/spec/heroku/command/orgs_spec.rb +++ b/spec/heroku/command/orgs_spec.rb @@ -16,8 +16,8 @@ module Heroku::Command context(:index) do it "displays a message when you have no org memberships" do stderr, stdout = execute("orgs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT You are not a member of any organizations. STDOUT end @@ -31,8 +31,8 @@ module Heroku::Command ) stderr, stdout = execute("orgs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT test-org collaborator test-org2 admin @@ -48,8 +48,8 @@ module Heroku::Command ) stderr, stdout = execute("orgs") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT test-org collaborator test-org2 admin, default @@ -60,28 +60,28 @@ module Heroku::Command context(:default) do context "when a target org is specified" do it "sets the default org to the target" do - org_api.should_receive(:set_default_org).with("test-org").once + expect(org_api).to receive(:set_default_org).with("test-org").once stderr, stdout = execute("orgs:default test-org") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting test-org as the default organization... done STDOUT end it "removes the default org when the org name is 'personal'" do - org_api.should_receive(:remove_default_org).once + expect(org_api).to receive(:remove_default_org).once stderr, stdout = execute("orgs:default personal") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting personal account as default... done STDOUT end it "removes the defautl org when the personal flag is passed" do - org_api.should_receive(:remove_default_org).once + expect(org_api).to receive(:remove_default_org).once stderr, stdout = execute("orgs:default --personal") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Setting personal account as default... done STDOUT end @@ -98,16 +98,16 @@ module Heroku::Command ) stderr, stdout = execute("orgs:default") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT test-org is the default organization. STDOUT end it "displays personal account as default when no org present" do stderr, stdout = execute("orgs:default") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Personal account is default. STDOUT end @@ -117,12 +117,12 @@ module Heroku::Command context(:open) do before(:each) do require("launchy") - ::Launchy.should_receive(:open).with("https://dashboard.heroku.com/orgs/test-org/apps").once.and_return("") + expect(::Launchy).to receive(:open).with("https://dashboard.heroku.com/orgs/test-org/apps").once.and_return("") end it "opens the org specified in an argument" do stderr, stdout = execute("orgs:open --org test-org") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Opening web interface for test-org... done STDOUT end @@ -136,7 +136,7 @@ module Heroku::Command ) stderr, stdout = execute("orgs:open") - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Opening web interface for test-org... done STDOUT end diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index dd89b357b..8ee5ce971 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -48,8 +48,8 @@ module Heroku::Command stub_pg.reset stderr, stdout = execute("pg:reset RONIN --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resetting HEROKU_POSTGRESQL_RONIN_URL... done STDOUT end @@ -58,15 +58,15 @@ module Heroku::Command stub_pg.reset stderr, stdout = execute("pg:reset RONIN") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Confirmation did not match example. Aborted. STDERR - stdout.should == " + expect(stdout).to eq(" ! WARNING: Destructive Action ! This command will affect the app: example ! To proceed, type \"example\" or re-run this command with --confirm example -> " +> ") end context "index" do @@ -83,8 +83,8 @@ module Heroku::Command ]) stderr, stdout = execute("pg") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === HEROKU_POSTGRESQL_FOLLOW_URL Plan: Ronin Status: available @@ -134,8 +134,8 @@ module Heroku::Command ]) stderr, stdout = execute("pg:info RONIN") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === HEROKU_POSTGRESQL_RONIN_URL Plan: Ronin Status: available @@ -154,20 +154,20 @@ module Heroku::Command context "promotion" do it "promotes the specified database" do stderr, stdout = execute("pg:promote RONIN --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Promoting HEROKU_POSTGRESQL_RONIN_URL to DATABASE_URL... done STDOUT - api.get_config_vars("example").body["DATABASE_URL"].should == "postgres://ronin_database_url" + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://ronin_database_url") end it "fails if no database is specified" do stderr, stdout = execute("pg:promote") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku pg:promote DATABASE ! Must specify DATABASE to promote. STDERR - stdout.should == "" + expect(stdout).to eq("") end end @@ -175,8 +175,8 @@ module Heroku::Command it "resets credentials and promotes to DATABASE_URL if it's the main DB" do stub_pg.rotate_credentials stderr, stdout = execute("pg:credentials iv --reset") - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT Resetting credentials for HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)... done Promoting HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)... done STDOUT @@ -189,8 +189,8 @@ module Heroku::Command "HEROKU_POSTGRESQL_RESETME_URL" => "postgres://something_else" } stderr, stdout = execute("pg:credentials follo --reset") - stderr.should == '' - stdout.should_not include("Promoting") + expect(stderr).to eq('') + expect(stdout).not_to include("Promoting") end end @@ -198,9 +198,9 @@ module Heroku::Command context "unfollow" do it "sends request to unfollow" do hpg_client = double('Heroku::Client::HerokuPostgresql') - Heroku::Client::HerokuPostgresql.should_receive(:new).twice.and_return(hpg_client) - hpg_client.should_receive(:unfollow) - hpg_client.should_receive(:get_database).and_return( + expect(Heroku::Client::HerokuPostgresql).to receive(:new).twice.and_return(hpg_client) + expect(hpg_client).to receive(:unfollow) + expect(hpg_client).to receive(:get_database).and_return( :following => 'postgresql://user:pass@roninhost/database', :info => [ {"name"=>"Plan", "values"=>["Ronin"]}, @@ -215,8 +215,8 @@ module Heroku::Command ] ) stderr, stdout = execute("pg:unfollow HEROKU_POSTGRESQL_FOLLOW_URL --confirm example") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ! HEROKU_POSTGRESQL_FOLLOW_URL will become writable and no longer ! follow Database on roninhost:5432/database. This cannot be undone. Unfollowing HEROKU_POSTGRESQL_FOLLOW_URL... done @@ -252,8 +252,8 @@ module Heroku::Command }) stderr, stdout = execute("pg:diagnose") - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT Report abc123 for appname::dbcolor available for one month after creation on 2014-06-24 01:26:11.941197+00 diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index b07e2504f..86a613e50 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -7,10 +7,10 @@ module Heroku::Command api.post_app("name" => "example") stub_core stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Your app has no databases. STDERR - stdout.should == "" + expect(stdout).to eq("") api.delete_app("example") end end @@ -18,7 +18,7 @@ module Heroku::Command describe Pgbackups do before do @pgbackups = prepare_command(Pgbackups) - @pgbackups.heroku.stub(:info).and_return({}) + allow(@pgbackups.heroku).to receive(:info).and_return({}) api.post_app("name" => "example") api.put_config_vars( @@ -64,8 +64,8 @@ module Heroku::Command }]) stderr, stdout = execute("pgbackups") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ID Backup Time Status Size Database ---- ------------------------- --------- ---- -------- b001 2012-01-01 12:00:01 +0000 Capturing 1024 DATABASE @@ -77,7 +77,7 @@ module Heroku::Command let(:from_url) { "postgres://from/bar" } let(:attachment) { double('attachment', :display_name => from_name, :url => from_url ) } before do - @pgbackups.stub(:resolve).and_return(attachment) + allow(@pgbackups).to receive(:resolve).and_return(attachment) end it "gets the url for the latest backup if nothing is specified" do @@ -85,34 +85,34 @@ module Heroku::Command stub_pgbackups.get_latest_backup.returns({"public_url" => "http://latest/backup.dump"}) old_stdout_isatty = STDOUT.isatty - $stdout.stub(:isatty).and_return(true) + allow($stdout).to receive(:isatty).and_return(true) stderr, stdout = execute("pgbackups:url") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT http://latest/backup.dump STDOUT - $stdout.stub(:isatty).and_return(old_stdout_isatty) + allow($stdout).to receive(:isatty).and_return(old_stdout_isatty) end it "gets the url for the named backup if a name is specified" do stub_pgbackups.get_backup.with("b001").returns({"public_url" => "http://latest/backup.dump" }) old_stdout_isatty = STDOUT.isatty - $stdout.stub(:isatty).and_return(true) + allow($stdout).to receive(:isatty).and_return(true) stderr, stdout = execute("pgbackups:url b001") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT http://latest/backup.dump STDOUT - $stdout.stub(:isatty).and_return(old_stdout_isatty) + allow($stdout).to receive(:isatty).and_return(old_stdout_isatty) end it "should capture a backup when requested" do backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - @pgbackups.stub(:args).and_return([]) - @pgbackups.stub(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) - @pgbackups.stub(:poll_transfer!).with(backup_obj).and_return(backup_obj) + allow(@pgbackups).to receive(:args).and_return([]) + allow(@pgbackups).to receive(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) + allow(@pgbackups).to receive(:poll_transfer!).with(backup_obj).and_return(backup_obj) @pgbackups.capture end @@ -120,9 +120,9 @@ module Heroku::Command it "should send expiration flag to client if specified on args" do backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - @pgbackups.stub(:options).and_return({:expire => true}) - @pgbackups.stub(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) - @pgbackups.stub(:poll_transfer!).with(backup_obj).and_return(backup_obj) + allow(@pgbackups).to receive(:options).and_return({:expire => true}) + allow(@pgbackups).to receive(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) + allow(@pgbackups).to receive(:poll_transfer!).with(backup_obj).and_return(backup_obj) @pgbackups.capture end @@ -130,11 +130,11 @@ module Heroku::Command it "destroys no backup without a name" do stub_core stderr, stdout = execute("pgbackups:destroy") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku pgbackups:destroy BACKUP_ID ! Must specify BACKUP_ID to destroy. STDERR - stdout.should == "" + expect(stdout).to eq("") end it "destroys a backup" do @@ -143,8 +143,8 @@ module Heroku::Command stub_pgbackups.delete_backup("b001").returns({}) stderr, stdout = execute("pgbackups:destroy b001") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Destroying b001... done STDOUT end @@ -172,11 +172,11 @@ def stub_failed_capture(log) it 'aborts on a generic error' do stub_failed_capture "something generic" stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! An error occurred and your backup did not finish. ! Please run `heroku logs --ps pgbackups` for details. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar @@ -187,12 +187,12 @@ def stub_failed_capture(log) it 'aborts and informs when the database isnt up yet' do stub_failed_capture 'could not translate host name "ec2-42-42-42-42.compute-1.amazonaws.com" to address: Name or service not known' stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! An error occurred and your backup did not finish. ! Please run `heroku logs --ps pgbackups` for details. ! The database is not yet online. Please try again. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar @@ -203,12 +203,12 @@ def stub_failed_capture(log) it 'aborts and informs when the credentials are incorrect' do stub_failed_capture 'psql: FATAL: database "randomname" does not exist' stderr, stdout = execute("pgbackups:capture") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! An error occurred and your backup did not finish. ! Please run `heroku logs --ps pgbackups` for details. ! The database credentials are incorrect. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar @@ -222,21 +222,21 @@ def stub_failed_capture(log) let(:attachment) { double('attachment', :display_name => 'someconfigvar', :url => 'postgres://fromhost/database') } before do @pgbackups_client = double("pgbackups_client") - @pgbackups.stub(:pgbackup_client).and_return(@pgbackups_client) + allow(@pgbackups).to receive(:pgbackup_client).and_return(@pgbackups_client) end it "should receive a confirm_command on restore" do - @pgbackups_client.stub(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } + allow(@pgbackups_client).to receive(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } - @pgbackups.should_receive(:confirm_command).and_return(false) - @pgbackups_client.should_not_receive(:transfer!) + expect(@pgbackups).to receive(:confirm_command).and_return(false) + expect(@pgbackups_client).not_to receive(:transfer!) @pgbackups.restore end it "aborts if no database addon is present" do - @pgbackups.should_receive(:resolve).and_raise(SystemExit) - lambda { @pgbackups.restore }.should raise_error(SystemExit) + expect(@pgbackups).to receive(:resolve).and_raise(SystemExit) + expect { @pgbackups.restore }.to raise_error(SystemExit) end context "for commands which perform restores" do @@ -248,13 +248,13 @@ def stub_failed_capture(log) "from_name" => "postgres://databasehost/dbname" } - @pgbackups.stub(:confirm_command).and_return(true) - @pgbackups_client.should_receive(:create_transfer).and_return(@backup_obj) - @pgbackups.stub(:poll_transfer!).and_return(@backup_obj) + allow(@pgbackups).to receive(:confirm_command).and_return(true) + expect(@pgbackups_client).to receive(:create_transfer).and_return(@backup_obj) + allow(@pgbackups).to receive(:poll_transfer!).and_return(@backup_obj) end it "should default to the latest backup" do - @pgbackups.stub(:args).and_return([]) + allow(@pgbackups).to receive(:args).and_return([]) mock(@pgbackups_client).get_latest_backup.returns(@backup_obj) @pgbackups.restore end @@ -263,20 +263,20 @@ def stub_failed_capture(log) it "should restore the named backup" do name = "backupname" args = ['DATABASE', name] - @pgbackups.stub(:args).and_return(args) - @pgbackups.stub(:shift_argument).and_return(*args) - @pgbackups.stub(:resolve).and_return(attachment) + allow(@pgbackups).to receive(:args).and_return(args) + allow(@pgbackups).to receive(:shift_argument).and_return(*args) + allow(@pgbackups).to receive(:resolve).and_return(attachment) mock(@pgbackups_client).get_backup.with(name).returns(@backup_obj) @pgbackups.restore end it "should handle external restores" do args = ['db_name_gets_shifted_out_in_resolve_db', 'http://external/file.dump'] - @pgbackups.stub(:args).and_return(args) - @pgbackups.stub(:shift_argument).and_return(*args) - @pgbackups.stub(:resolve).and_return(attachment) - @pgbackups_client.should_not_receive(:get_backup) - @pgbackups_client.should_not_receive(:get_latest_backup) + allow(@pgbackups).to receive(:args).and_return(args) + allow(@pgbackups).to receive(:shift_argument).and_return(*args) + allow(@pgbackups).to receive(:resolve).and_return(attachment) + expect(@pgbackups_client).not_to receive(:get_backup) + expect(@pgbackups_client).not_to receive(:get_latest_backup) @pgbackups.restore end end @@ -293,19 +293,19 @@ def stub_error_backup_with_log(log) "log" => log } - @pgbackups_client.should_receive(:create_transfer) { @backup_obj } - @pgbackups.stub(:poll_transfer!) { @backup_obj } + expect(@pgbackups_client).to receive(:create_transfer) { @backup_obj } + allow(@pgbackups).to receive(:poll_transfer!) { @backup_obj } end it 'aborts for a generic error' do stub_error_backup_with_log 'something generic' - @pgbackups.should_receive(:error).with("An error occurred and your restore did not finish.\nPlease run `heroku logs --ps pgbackups` for details.") + expect(@pgbackups).to receive(:error).with("An error occurred and your restore did not finish.\nPlease run `heroku logs --ps pgbackups` for details.") @pgbackups.restore end it 'aborts and informs for expired s3 urls' do stub_error_backup_with_log 'Invalid dump format: /tmp/aDMyoXPrAX/b031.dump: XML document text' - @pgbackups.should_receive(:error).with(/backup url is invalid/) + expect(@pgbackups).to receive(:error).with(/backup url is invalid/) @pgbackups.restore end end diff --git a/spec/heroku/command/plugins_spec.rb b/spec/heroku/command/plugins_spec.rb index d45c03fdc..48dfd65ae 100644 --- a/spec/heroku/command/plugins_spec.rb +++ b/spec/heroku/command/plugins_spec.rb @@ -13,25 +13,25 @@ module Heroku::Command context("install") do before do - Heroku::Plugin.should_receive(:new).with('git://github.com/heroku/Plugin.git').and_return(@plugin) - @plugin.should_receive(:install).and_return(true) + expect(Heroku::Plugin).to receive(:new).with('git://github.com/heroku/Plugin.git').and_return(@plugin) + expect(@plugin).to receive(:install).and_return(true) end it "installs plugins" do - Heroku::Plugin.should_receive(:load_plugin).and_return(true) + expect(Heroku::Plugin).to receive(:load_plugin).and_return(true) stderr, stdout = execute("plugins:install git://github.com/heroku/Plugin.git") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Installing Plugin... done STDOUT end it "does not install plugins that do not load" do - Heroku::Plugin.should_receive(:load_plugin).and_return(false) - @plugin.should_receive(:uninstall).and_return(true) + expect(Heroku::Plugin).to receive(:load_plugin).and_return(false) + expect(@plugin).to receive(:uninstall).and_return(true) stderr, stdout = execute("plugins:install git://github.com/heroku/Plugin.git") - stderr.should == '' # normally would have error, but mocks/stubs don't allow - stdout.should == "Installing Plugin... " # also inaccurate, would end in ' failed' + expect(stderr).to eq('') # normally would have error, but mocks/stubs don't allow + expect(stdout).to eq("Installing Plugin... ") # also inaccurate, would end in ' failed' end end @@ -39,24 +39,24 @@ module Heroku::Command context("uninstall") do before do - Heroku::Plugin.should_receive(:new).with('Plugin').and_return(@plugin) + expect(Heroku::Plugin).to receive(:new).with('Plugin').and_return(@plugin) end it "uninstalls plugins" do - @plugin.should_receive(:uninstall).and_return(true) + expect(@plugin).to receive(:uninstall).and_return(true) stderr, stdout = execute("plugins:uninstall Plugin") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Uninstalling Plugin... done STDOUT end it "does not uninstall plugins that do not exist" do stderr, stdout = execute("plugins:uninstall Plugin") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Plugin plugin not found. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Uninstalling Plugin... failed STDOUT end @@ -66,34 +66,34 @@ module Heroku::Command context("update") do before do - Heroku::Plugin.should_receive(:new).with('Plugin').and_return(@plugin) + expect(Heroku::Plugin).to receive(:new).with('Plugin').and_return(@plugin) end it "updates plugin by name" do - @plugin.should_receive(:update).and_return(true) + expect(@plugin).to receive(:update).and_return(true) stderr, stdout = execute("plugins:update Plugin") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Updating Plugin... done STDOUT end it "updates all plugins" do - Heroku::Plugin.stub(:list).and_return(['Plugin']) - @plugin.should_receive(:update).and_return(true) + allow(Heroku::Plugin).to receive(:list).and_return(['Plugin']) + expect(@plugin).to receive(:update).and_return(true) stderr, stdout = execute("plugins:update") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Updating Plugin... done STDOUT end it "does not update plugins that do not exist" do stderr, stdout = execute("plugins:update Plugin") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Plugin plugin not found. STDERR - stdout.should == <<-STDOUT + expect(stdout).to eq <<-STDOUT Updating Plugin... failed STDOUT end diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index 3949a4b5d..e7bd38424 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -18,11 +18,11 @@ end it "ps:dynos errors out on cedar apps" do - lambda { execute("ps:dynos") }.should raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") + expect { execute("ps:dynos") }.to raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") end it "ps:workers errors out on cedar apps" do - lambda { execute("ps:workers") }.should raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") + expect { execute("ps:workers") }.to raise_error(Heroku::Command::CommandFailed, "For Cedar apps, use `heroku ps`") end describe "ps" do @@ -44,10 +44,10 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).exactly(10).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(10).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === web (1X): `bundle exec thin start -p $PORT` web.1: created 2012/09/11 12:34:56 (~ 0s ago) web.2: created 2012/09/11 12:34:56 (~ 0s ago) @@ -80,10 +80,10 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).twice.and_return('2012/09/11 12:34:56 (~ 0s ago)') + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return('2012/09/11 12:34:56 (~ 0s ago)') stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === run: one-off processes run.1 (1X): created 2012/09/11 12:34:56 (~ 0s ago): `bash` run.2 (1X): created 2012/09/11 12:34:56 (~ 0s ago): `bash` @@ -108,11 +108,11 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === web (2X): `bundle exec thin start -p $PORT` web.1: created 2012/09/11 12:34:56 (~ 0s ago) web.2: created 2012/09/11 12:34:56 (~ 0s ago) @@ -137,11 +137,11 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === web (PX): `bundle exec thin start -p $PORT` web.1: created 2012/09/11 12:34:56 (~ 0s ago) web.2: created 2012/09/11 12:34:56 (~ 0s ago) @@ -167,10 +167,10 @@ end.to_json, :status => 200 ) - Heroku::Command::Ps.any_instance.should_receive(:time_ago).exactly(4).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(4).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === run: one-off processes run.1 (PX): created 2012/09/11 12:34:56 (~ 0s ago): `bash` run.2 (2X): created 2012/09/11 12:34:56 (~ 0s ago): `bash` @@ -187,24 +187,24 @@ it "restarts all dynos with no args" do stderr, stdout = execute("ps:restart") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting dynos... done STDOUT end it "restarts one dyno" do stderr, stdout = execute("ps:restart web.1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting web.1 dyno... done STDOUT end it "restarts a type of dyno" do stderr, stdout = execute("ps:restart web") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting web dynos... done STDOUT end @@ -218,8 +218,8 @@ { :body => [{"quantity" => "5", "size" => "1X", "type" => "web"}], :status => 200}) stderr, stdout = execute("ps:scale web=5") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 5:1X. STDOUT end @@ -229,8 +229,8 @@ { :body => [{"quantity" => "3", "size" => "1X", "type" => "web"}], :status => 200}) stderr, stdout = execute("ps:scale web+2") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 3:1X. STDOUT end @@ -247,8 +247,8 @@ :status => 200 ) stderr, stdout = execute("ps:scale web=4:2X") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 4:2X. STDOUT end @@ -272,8 +272,8 @@ :status => 200 ) stderr, stdout = execute("ps:scale web=4:1X worker=2:2x") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 4:1X, worker at 2:2X. STDOUT end @@ -290,8 +290,8 @@ :status => 200 ) stderr, stdout = execute("ps:scale web=4:PX") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Scaling dynos... done, now running web at 4:PX. STDOUT end @@ -311,8 +311,8 @@ :status => 200 ) stderr, stdout = execute("ps:resize web=2X") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done web dynos now 2X ($0.10/dyno-hour) STDOUT @@ -336,8 +336,8 @@ :status => 200 ) stderr, stdout = execute("ps:resize web=4x worker=2X") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done web dynos now 4X ($0.20/dyno-hour) worker dynos now 2X ($0.10/dyno-hour) @@ -362,8 +362,8 @@ :status => 200 ) stderr, stdout = execute("ps:resize web=PX worker=Px") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done web dynos now PX ($0.80/dyno-hour) worker dynos now PX ($0.80/dyno-hour) @@ -376,16 +376,16 @@ it "restarts one dyno" do stderr, stdout = execute("ps:restart ps.1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting ps.1 dyno... done STDOUT end it "restarts a type of dyno" do stderr, stdout = execute("ps:restart ps") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Restarting ps dynos... done STDOUT end @@ -408,8 +408,8 @@ it "displays the current number of dynos" do stderr, stdout = execute("ps:dynos") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:dynos QTY` has been deprecated and replaced with `heroku ps:scale dynos=QTY` example is running 0 dynos STDOUT @@ -417,8 +417,8 @@ it "sets the number of dynos" do stderr, stdout = execute("ps:dynos 5") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:dynos QTY` has been deprecated and replaced with `heroku ps:scale dynos=QTY` Scaling dynos... done, now running 5 STDOUT @@ -430,8 +430,8 @@ it "displays the current number of workers" do stderr, stdout = execute("ps:workers") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:workers QTY` has been deprecated and replaced with `heroku ps:scale workers=QTY` example is running 0 workers STDOUT @@ -439,8 +439,8 @@ it "sets the number of workers" do stderr, stdout = execute("ps:workers 5") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT ~ `heroku ps:workers QTY` has been deprecated and replaced with `heroku ps:scale workers=QTY` Scaling workers... done, now running 5 STDOUT diff --git a/spec/heroku/command/releases_spec.rb b/spec/heroku/command/releases_spec.rb index 8e600a8ec..fb75c5db9 100644 --- a/spec/heroku/command/releases_spec.rb +++ b/spec/heroku/command/releases_spec.rb @@ -23,10 +23,10 @@ end it "should list releases" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).exactly(5).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)', '2012/09/10 10:36:44 (~ 1h ago)', '2012/01/02 12:34:56') + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).exactly(5).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)', '2012/09/10 10:36:44 (~ 1h ago)', '2012/01/02 12:34:56') @stderr, @stdout = execute("releases") - @stderr.should == "" - @stdout.should == <<-STDOUT + expect(@stderr).to eq("") + expect(@stdout).to eq <<-STDOUT === example Releases v5 Config add SUPER_LONG_CONFIG_VAR_TO_GE.. email@example.com 2012/09/10 11:36:44 (~ 0s ago) v4 Config add QUX_QUUX email@example.com 2012/09/10 11:36:43 (~ 1s ago) @@ -38,10 +38,10 @@ end it "should list a specified number of releases" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).exactly(3).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)') + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).exactly(3).times.and_return('2012/09/10 11:36:44 (~ 0s ago)', '2012/09/10 11:36:43 (~ 1s ago)', '2012/09/10 11:35:44 (~ 1m ago)') @stderr, @stdout = execute("releases -n 3") - @stderr.should == "" - @stdout.should == <<-STDOUT + expect(@stderr).to eq("") + expect(@stdout).to eq <<-STDOUT === example Releases v5 Config add SUPER_LONG_CONFIG_VAR_TO_GE.. email@example.com 2012/09/10 11:36:44 (~ 0s ago) v4 Config add QUX_QUUX email@example.com 2012/09/10 11:36:43 (~ 1s ago) @@ -63,17 +63,17 @@ it "requires a release to be specified" do stderr, stdout = execute("releases:info") - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! Usage: heroku releases:info RELEASE STDERR - stdout.should == "" + expect(stdout).to eq("") end it "shows info for a single release" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("releases:info v1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Release v1 By: email@example.com Change: Config add FOO_BAR @@ -88,10 +88,10 @@ end it "shows info for a single release in shell compatible format" do - Heroku::Command::Releases.any_instance.should_receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Releases).to receive(:time_ago).and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("releases:info v1 --shell") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Release v1 By: email@example.com Change: Config add FOO_BAR @@ -120,16 +120,16 @@ it "rolls back to the latest release with no argument" do stderr, stdout = execute("releases:rollback") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Rolling back example... done, v2 STDOUT end it "rolls back to the specified release" do stderr, stdout = execute("releases:rollback v1") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Rolling back example... done, v1 STDOUT end diff --git a/spec/heroku/command/run_spec.rb b/spec/heroku/command/run_spec.rb index 80a45dc84..966419faa 100644 --- a/spec/heroku/command/run_spec.rb +++ b/spec/heroku/command/run_spec.rb @@ -20,8 +20,8 @@ stub_rendezvous.start { $stdout.puts "output" } stderr, stdout = execute("run bin/foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Running `bin/foo` attached to terminal... up, run.1 output STDOUT @@ -31,8 +31,8 @@ describe "run:detached" do it "runs a command detached" do stderr, stdout = execute("run:detached bin/foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Running `bin/foo` detached... up, run.1 Use `heroku logs -p run.1` to view the output. STDOUT @@ -52,8 +52,8 @@ stub_rendezvous.start { $stdout.puts("rake_output") } stderr, stdout = execute("run:rake foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT WARNING: `heroku run:rake` has been deprecated. Please use `heroku run rake` instead. Running `rake foo` attached to terminal... up, run.1 rake_output @@ -64,8 +64,8 @@ stub_rendezvous.start { $stdout.puts("rake_output") } stderr, stdout = execute("rake foo") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT WARNING: `heroku rake` has been deprecated. Please use `heroku run rake` instead. Running `rake foo` attached to terminal... up, run.1 rake_output @@ -76,8 +76,8 @@ describe "run:console" do it "has been removed" do stderr, stdout = execute("run:console") - stderr.should == "" - stdout.should =~ /has been removed/ + expect(stderr).to eq("") + expect(stdout).to match(/has been removed/) end end end diff --git a/spec/heroku/command/sharing_spec.rb b/spec/heroku/command/sharing_spec.rb index 029696226..080506f82 100644 --- a/spec/heroku/command/sharing_spec.rb +++ b/spec/heroku/command/sharing_spec.rb @@ -18,8 +18,8 @@ module Heroku::Command it "lists collaborators" do api.post_collaborator("example", "collaborator@example.com") stderr, stdout = execute("sharing") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Access List collaborator@example.com collaborator email@example.com collaborator @@ -31,8 +31,8 @@ module Heroku::Command it "adds collaborators with default access to view only" do stderr, stdout = execute("sharing:add collaborator@example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Adding collaborator@example.com to example as collaborator... done STDOUT end @@ -40,8 +40,8 @@ module Heroku::Command it "removes collaborators" do api.post_collaborator("example", "collaborator@example.com") stderr, stdout = execute("sharing:remove collaborator@example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Removing collaborator@example.com from example collaborators... done STDOUT end @@ -49,8 +49,8 @@ module Heroku::Command it "transfers ownership" do api.post_collaborator("example", "collaborator@example.com") stderr, stdout = execute("sharing:transfer collaborator@example.com") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Transferring example to collaborator@example.com... done STDOUT end diff --git a/spec/heroku/command/stack_spec.rb b/spec/heroku/command/stack_spec.rb index 7aba8d710..c0cc016ac 100644 --- a/spec/heroku/command/stack_spec.rb +++ b/spec/heroku/command/stack_spec.rb @@ -15,8 +15,8 @@ module Heroku::Command it "index should provide list" do stderr, stdout = execute("stack") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === example Available Stacks aspen-mri-1.8.6 bamboo-ree-1.8.7 @@ -28,8 +28,8 @@ module Heroku::Command it "migrate should succeed" do stderr, stdout = execute("stack:migrate bamboo-ree-1.8.7") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Stack set. Next release on example will use bamboo-ree-1.8.7. Run `git push heroku master` to create a new release on bamboo-ree-1.8.7. STDOUT diff --git a/spec/heroku/command/status_spec.rb b/spec/heroku/command/status_spec.rb index 9ab629a5a..01b0ea331 100644 --- a/spec/heroku/command/status_spec.rb +++ b/spec/heroku/command/status_spec.rb @@ -21,11 +21,11 @@ module Heroku::Command } ) - Heroku::Command::Status.any_instance.should_receive(:time_ago).and_return('2012/09/11 09:34:56 (~ 3h ago)', '2012/09/11 12:33:56 (~ 1m ago)', '2012/09/11 12:29:56 (~ 5m ago)', '2012/09/11 12:24:56 (~ 10m ago)', '2012/09/11 12:04:56 (~ 30m ago)', '2012/09/11 11:34:56 (~ 1h ago)', '2012/09/11 10:34:56 (~ 2h ago)', '2012/09/11 09:34:56 (~ 3h ago)') + expect_any_instance_of(Heroku::Command::Status).to receive(:time_ago).and_return('2012/09/11 09:34:56 (~ 3h ago)', '2012/09/11 12:33:56 (~ 1m ago)', '2012/09/11 12:29:56 (~ 5m ago)', '2012/09/11 12:24:56 (~ 10m ago)', '2012/09/11 12:04:56 (~ 30m ago)', '2012/09/11 11:34:56 (~ 1h ago)', '2012/09/11 10:34:56 (~ 2h ago)', '2012/09/11 09:34:56 (~ 3h ago)') stderr, stdout = execute("status") - stderr.should == '' - stdout.should == <<-STDOUT + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT === Heroku Status Development: red Production: red diff --git a/spec/heroku/command/version_spec.rb b/spec/heroku/command/version_spec.rb index cdd8c09bf..eb60f1571 100644 --- a/spec/heroku/command/version_spec.rb +++ b/spec/heroku/command/version_spec.rb @@ -6,8 +6,8 @@ module Heroku::Command it "shows version info" do stderr, stdout = execute("version") - stderr.should == "" - stdout.should == <<-STDOUT + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT #{Heroku.user_agent} STDOUT end diff --git a/spec/heroku/command_spec.rb b/spec/heroku/command_spec.rb index ff3e277ab..8e6581566 100644 --- a/spec/heroku/command_spec.rb +++ b/spec/heroku/command_spec.rb @@ -41,9 +41,9 @@ def to_s context "and the user includes --confirm APP --app APP2" do it "should warn that the app and confirm do not match and not continue" do - capture_stderr do + expect(capture_stderr do run "addons:add my_addon --confirm APP --app APP2" - end.should == " ! Mismatch between --app and --confirm\n" + end).to eq(" ! Mismatch between --app and --confirm\n") end end end @@ -83,11 +83,11 @@ def to_s end it "should not continue if the confirmation does not match" do - Heroku::Command.stub(:current_options).and_return(:confirm => 'not_example') + allow(Heroku::Command).to receive(:current_options).and_return(:confirm => 'not_example') - lambda do + expect do Heroku::Command.confirm_command('example') - end.should raise_error(Heroku::Command::CommandFailed) + end.to raise_error(Heroku::Command::CommandFailed) end it "should not continue if the user doesn't confirm" do @@ -104,41 +104,41 @@ def to_s describe "parsing errors" do it "extracts error messages from response when available in XML" do - Heroku::Command.extract_error('Invalid app name').should == 'Invalid app name' + expect(Heroku::Command.extract_error('Invalid app name')).to eq('Invalid app name') end it "extracts error messages from response when available in JSON" do - Heroku::Command.extract_error("{\"error\":\"Invalid app name\"}").should == 'Invalid app name' + expect(Heroku::Command.extract_error("{\"error\":\"Invalid app name\"}")).to eq('Invalid app name') end it "extracts error messages from response when available in plain text" do response = FakeResponse.new(:body => "Invalid app name", :headers => { :content_type => "text/plain; charset=UTF8" }) - Heroku::Command.extract_error(response).should == 'Invalid app name' + expect(Heroku::Command.extract_error(response)).to eq('Invalid app name') end it "shows Internal Server Error when the response doesn't contain a XML or JSON" do - Heroku::Command.extract_error('

    HTTP 500

    ').should == "Internal server error.\nRun `heroku status` to check for known platform issues." + expect(Heroku::Command.extract_error('

    HTTP 500

    ')).to eq("Internal server error.\nRun `heroku status` to check for known platform issues.") end it "shows Internal Server Error when the response is not plain text" do response = FakeResponse.new(:body => "Foobar", :headers => { :content_type => "application/xml" }) - Heroku::Command.extract_error(response).should == "Internal server error.\nRun `heroku status` to check for known platform issues." + expect(Heroku::Command.extract_error(response)).to eq("Internal server error.\nRun `heroku status` to check for known platform issues.") end it "allows a block to redefine the default error" do - Heroku::Command.extract_error("Foobar") { "Ok!" }.should == 'Ok!' + expect(Heroku::Command.extract_error("Foobar") { "Ok!" }).to eq('Ok!') end it "doesn't format the response if set to raw" do - Heroku::Command.extract_error("Foobar", :raw => true) { "Ok!" }.should == 'Ok!' + expect(Heroku::Command.extract_error("Foobar", :raw => true) { "Ok!" }).to eq('Ok!') end it "handles a nil body in parse_error_xml" do - lambda { Heroku::Command.parse_error_xml(nil) }.should_not raise_error + expect { Heroku::Command.parse_error_xml(nil) }.not_to raise_error end it "handles a nil body in parse_error_json" do - lambda { Heroku::Command.parse_error_json(nil) }.should_not raise_error + expect { Heroku::Command.parse_error_json(nil) }.not_to raise_error end end @@ -149,27 +149,27 @@ class Heroku::Command::Test::Multiple; end require "heroku/command/help" require "heroku/command/apps" - Heroku::Command.parse("unknown").should be_nil - Heroku::Command.parse("list").should include(:klass => Heroku::Command::Apps, :method => :index) - Heroku::Command.parse("apps").should include(:klass => Heroku::Command::Apps, :method => :index) - Heroku::Command.parse("apps:create").should include(:klass => Heroku::Command::Apps, :method => :create) + expect(Heroku::Command.parse("unknown")).to be_nil + expect(Heroku::Command.parse("list")).to include(:klass => Heroku::Command::Apps, :method => :index) + expect(Heroku::Command.parse("apps")).to include(:klass => Heroku::Command::Apps, :method => :index) + expect(Heroku::Command.parse("apps:create")).to include(:klass => Heroku::Command::Apps, :method => :create) end context "help" do it "works as a prefix" do - heroku("help ps:scale").should =~ /scale dynos by/ + expect(heroku("help ps:scale")).to match(/scale dynos by/) end it "works as an option" do - heroku("ps:scale -h").should =~ /scale dynos by/ - heroku("ps:scale --help").should =~ /scale dynos by/ + expect(heroku("ps:scale -h")).to match(/scale dynos by/) + expect(heroku("ps:scale --help")).to match(/scale dynos by/) end end context "when no commands match" do it "displays the version if --version is used" do - heroku("--version").should == <<-STDOUT + expect(heroku("--version")).to eq <<-STDOUT #{Heroku.user_agent} STDOUT end @@ -182,12 +182,12 @@ class Heroku::Command::Test::Multiple; end execute("aps") rescue SystemExit end - captured_stderr.string.should == <<-STDERR + expect(captured_stderr.string).to eq <<-STDERR ! `aps` is not a heroku command. ! Perhaps you meant `apps` or `ps`. ! See `heroku help` for a list of available commands. STDERR - captured_stdout.string.should == "" + expect(captured_stdout.string).to eq("") $stderr, $stdout = original_stderr, original_stdout end @@ -199,11 +199,11 @@ class Heroku::Command::Test::Multiple; end execute("sandwich") rescue SystemExit end - captured_stderr.string.should == <<-STDERR + expect(captured_stderr.string).to eq <<-STDERR ! `sandwich` is not a heroku command. ! See `heroku help` for a list of available commands. STDERR - captured_stdout.string.should == "" + expect(captured_stdout.string).to eq("") $stderr, $stdout = original_stderr, original_stdout end diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index 4d5d26325..3720d3732 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -7,8 +7,8 @@ before do @resolver = described_class.new('appname', double(:api)) - @resolver.stub(:app_config_vars) { app_config_vars } - @resolver.stub(:app_attachments) { app_attachments } + allow(@resolver).to receive(:app_config_vars) { app_config_vars } + allow(@resolver).to receive(:app_attachments) { app_attachments } end let(:app_config_vars) do @@ -47,37 +47,37 @@ it "resolves DATABASE" do att = @resolver.resolve('DATABASE') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end end context "when no app is specified or inferred, and identifier does not have app::db shorthand" do it 'exits, complaining about the missing app' do api = double('api') - api.stub(:get_attachments).and_raise("getting this far will cause an inaccurate 'internal server error' message") + allow(api).to receive(:get_attachments).and_raise("getting this far will cause an inaccurate 'internal server error' message") no_app_resolver = described_class.new(nil, api) - no_app_resolver.should_receive(:error).with(/No app specified/).and_raise(SystemExit) + expect(no_app_resolver).to receive(:error).with(/No app specified/).and_raise(SystemExit) expect { no_app_resolver.resolve('black') }.to raise_error(SystemExit) end end context "when the identifier has ::" do it 'changes the resolver app to the left of the ::' do - @resolver.app_name.should == 'appname' + expect(@resolver.app_name).to eq('appname') att = @resolver.resolve('app2::black') - @resolver.app_name.should == 'app2' + expect(@resolver.app_name).to eq('app2') end it 'resolves database names on the right of the ::' do att = @resolver.resolve('app2::black') - att.url.should == "postgres://black" # since we're mocking out the app_config_vars + expect(att.url).to eq("postgres://black") # since we're mocking out the app_config_vars end it 'looks allows nothing after the :: to use the default' do att = @resolver.resolve('app2::', 'DATABASE_URL') - att.url.should == "postgres://default" + expect(att.url).to eq("postgres://default") end end @@ -93,86 +93,86 @@ it "resolves DATABASE" do att = @resolver.resolve('DATABASE') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end end it "resolves default using NAME" do att = @resolver.resolve('IVORY') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "resolves non-default using NAME" do att = @resolver.resolve('BLACK') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "resolves default using NAME_URL" do att = @resolver.resolve('IVORY_URL') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "resolves non-default using NAME_URL" do att = @resolver.resolve('BLACK_URL') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "resolves default using lowercase" do att = @resolver.resolve('ivory') - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "resolves non-default using lowercase" do att = @resolver.resolve('black') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "resolves non-default using part of name" do att = @resolver.resolve('bla') - att.display_name.should == "HEROKU_POSTGRESQL_BLACK_URL" - att.url.should == "postgres://black" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_BLACK_URL") + expect(att.url).to eq("postgres://black") end it "throws an error if it doesnt exist" do - @resolver.should_receive(:error).with("Unknown database: violet. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database: violet. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") @resolver.resolve("violet") end context "default" do it "errors if there is no default" do - @resolver.should_receive(:error).with("Unknown database. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") @resolver.resolve(nil) end it "uses the default if nothing(nil) specified" do att = @resolver.resolve(nil, "DATABASE_URL") - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it "uses the default if nothing(empty) specified" do att = @resolver.resolve('', "DATABASE_URL") - att.display_name.should == "HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)" - att.url.should == "postgres://default" + expect(att.display_name).to eq("HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL)") + expect(att.url).to eq("postgres://default") end it 'throws an error if given an empty string and asked for the default and there is no default' do app_config_vars.delete 'DATABASE_URL' - @resolver.should_receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end it 'throws an error if given an empty string and asked for the default and the default doesnt match' do app_config_vars['DATABASE_URL'] = 'something different' - @resolver.should_receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index fc7925ec0..9dfb8dd60 100644 --- a/spec/heroku/helpers_spec.rb +++ b/spec/heroku/helpers_spec.rb @@ -8,9 +8,9 @@ module Heroku context "display_object" do it "should display Array correctly" do - capture_stdout do + expect(capture_stdout do display_object([1,2,3]) - end.should == <<-OUT + end).to eq <<-OUT 1 2 3 @@ -18,9 +18,9 @@ module Heroku end it "should display { :header => [] } list correctly" do - capture_stdout do + expect(capture_stdout do display_object({:first_header => [1,2,3], :last_header => [7,8,9]}) - end.should == <<-OUT + end).to eq <<-OUT === first_header 1 2 @@ -35,9 +35,9 @@ module Heroku end it "should display String properly" do - capture_stdout do + expect(capture_stdout do display_object('string') - end.should == <<-OUT + end).to eq <<-OUT string OUT end diff --git a/spec/heroku/plugin_spec.rb b/spec/heroku/plugin_spec.rb index 33afda590..550cf30b2 100644 --- a/spec/heroku/plugin_spec.rb +++ b/spec/heroku/plugin_spec.rb @@ -6,20 +6,20 @@ module Heroku include SandboxHelper it "lives in ~/.heroku/plugins" do - Plugin.stub(:home_directory).and_return('/home/user') - Plugin.directory.should == '/home/user/.heroku/plugins' + allow(Plugin).to receive(:home_directory).and_return('/home/user') + expect(Plugin.directory).to eq('/home/user/.heroku/plugins') end it "extracts the name from git urls" do - Plugin.new('git://github.com/heroku/plugin.git').name.should == 'plugin' + expect(Plugin.new('git://github.com/heroku/plugin.git').name).to eq('plugin') end describe "management" do before(:each) do @sandbox = "/tmp/heroku_plugins_spec_#{Process.pid}" FileUtils.mkdir_p(@sandbox) - Dir.stub(:pwd).and_return(@sandbox) - Plugin.stub(:directory).and_return(@sandbox) + allow(Dir).to receive(:pwd).and_return(@sandbox) + allow(Plugin).to receive(:directory).and_return(@sandbox) end after(:each) do @@ -29,8 +29,8 @@ module Heroku it "lists installed plugins" do FileUtils.mkdir_p(@sandbox + '/plugin1') FileUtils.mkdir_p(@sandbox + '/plugin2') - Plugin.list.should include 'plugin1' - Plugin.list.should include 'plugin2' + expect(Plugin.list).to include 'plugin1' + expect(Plugin.list).to include 'plugin2' end it "installs pulling from the plugin url" do @@ -38,8 +38,8 @@ module Heroku FileUtils.mkdir_p(plugin_folder) `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_truthy - File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("test\n") end it "reinstalls over old copies" do @@ -48,8 +48,8 @@ module Heroku `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` Plugin.new(plugin_folder).install Plugin.new(plugin_folder).install - File.directory?("#{@sandbox}/heroku_plugin").should be_truthy - File.read("#{@sandbox}/heroku_plugin/README").should == "test\n" + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("test\n") end context "update" do @@ -64,8 +64,8 @@ module Heroku it "updates existing copies" do Plugin.new('heroku_plugin').update - File.directory?("#{@sandbox}/heroku_plugin").should be_truthy - File.read("#{@sandbox}/heroku_plugin/README").should == "updated\n" + expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("updated\n") end it "warns on legacy plugins" do @@ -76,7 +76,7 @@ module Heroku rescue SystemExit end end - stderr.should == <<-STDERR + expect(stderr).to eq <<-STDERR ! heroku_plugin is a legacy plugin installation. ! Enable updating by reinstalling with `heroku plugins:install`. STDERR @@ -84,7 +84,7 @@ module Heroku it "raises exception on symlinked plugins" do `cd #{@sandbox} && ln -s heroku_plugin heroku_plugin_symlink` - lambda { Plugin.new('heroku_plugin_symlink').update }.should raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin + expect { Plugin.new('heroku_plugin_symlink').update }.to raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin end end @@ -93,7 +93,7 @@ module Heroku it "uninstalls removing the folder" do FileUtils.mkdir_p(@sandbox + '/plugin1') Plugin.new('git://github.com/heroku/plugin1.git').uninstall - Plugin.list.should == [] + expect(Plugin.list).to eq([]) end it "adds the lib folder in the plugin to the load path, if present" do @@ -107,7 +107,7 @@ module Heroku FileUtils.mkdir_p(@sandbox + '/plugin') File.open(@sandbox + '/plugin/init.rb', 'w') { |f| f.write "LoadedInit = true" } Plugin.load! - LoadedInit.should be_truthy + expect(LoadedInit).to be_truthy end describe "when there are plugin load errors" do @@ -118,7 +118,7 @@ module Heroku it "should not throw an error" do capture_stderr do - lambda { Plugin.load! }.should_not raise_error + expect { Plugin.load! }.not_to raise_error end end @@ -126,7 +126,7 @@ module Heroku stderr = capture_stderr do Plugin.load! end - stderr.should include('some_non_existant_file (LoadError)') + expect(stderr).to include('some_non_existant_file (LoadError)') end it "should still load other plugins" do @@ -135,8 +135,8 @@ module Heroku stderr = capture_stderr do Plugin.load! end - stderr.should include('some_non_existant_file (LoadError)') - LoadedPlugin2.should be_truthy + expect(stderr).to include('some_non_existant_file (LoadError)') + expect(LoadedPlugin2).to be_truthy end end @@ -151,20 +151,20 @@ module Heroku it "should show confirmation to remove deprecated plugins if in an interactive shell" do old_stdin_isatty = STDIN.isatty - STDIN.stub(:isatty).and_return(true) - Plugin.should_receive(:confirm).with("The plugin heroku-releases has been deprecated. Would you like to remove it? (y/N)").and_return(true) - Plugin.should_receive(:remove_plugin).with("heroku-releases") + allow(STDIN).to receive(:isatty).and_return(true) + expect(Plugin).to receive(:confirm).with("The plugin heroku-releases has been deprecated. Would you like to remove it? (y/N)").and_return(true) + expect(Plugin).to receive(:remove_plugin).with("heroku-releases") Plugin.load! - STDIN.stub(:isatty).and_return(old_stdin_isatty) + allow(STDIN).to receive(:isatty).and_return(old_stdin_isatty) end it "should not prompt for deprecation if not in an interactive shell" do old_stdin_isatty = STDIN.isatty - STDIN.stub(:isatty).and_return(false) - Plugin.should_not_receive(:confirm) - Plugin.should_not_receive(:remove_plugin).with("heroku-releases") + allow(STDIN).to receive(:isatty).and_return(false) + expect(Plugin).not_to receive(:confirm) + expect(Plugin).not_to receive(:remove_plugin).with("heroku-releases") Plugin.load! - STDIN.stub(:isatty).and_return(old_stdin_isatty) + allow(STDIN).to receive(:isatty).and_return(old_stdin_isatty) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c8d4a4a2d..490204e75 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -38,12 +38,12 @@ def stub_api_request(method, path) def prepare_command(klass) command = klass.new - command.stub(:app).and_return("example") - command.stub(:ask).and_return("") - command.stub(:display) - command.stub(:hputs) - command.stub(:hprint) - command.stub(:heroku).and_return(double('heroku client', :host => 'heroku.com')) + allow(command).to receive(:app).and_return("example") + allow(command).to receive(:ask).and_return("") + allow(command).to receive(:display) + allow(command).to receive(:hputs) + allow(command).to receive(:hprint) + allow(command).to receive(:heroku).and_return(double('heroku client', :host => 'heroku.com')) command end diff --git a/spec/support/openssl_mock_helper.rb b/spec/support/openssl_mock_helper.rb index 0bf9621f8..fda0e6a76 100644 --- a/spec/support/openssl_mock_helper.rb +++ b/spec/support/openssl_mock_helper.rb @@ -3,6 +3,6 @@ def mock_openssl @tcp_socket_mock = double "TCPSocket", :close => true @ssl_socket_mock = double "SSLSocket", :sync= => true, :connect => true, :close => true, :to_io => $stdin - OpenSSL::SSL::SSLSocket.stub(:new).and_return(@ssl_socket_mock) - OpenSSL::SSL::SSLContext.stub(:new).and_return(@ctx_mock) + allow(OpenSSL::SSL::SSLSocket).to receive(:new).and_return(@ssl_socket_mock) + allow(OpenSSL::SSL::SSLContext).to receive(:new).and_return(@ctx_mock) end From c62009a817d799a9f69371444109cb561ad86250 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 10:49:22 -0700 Subject: [PATCH 024/952] upgrade webmock to fix rspec deprecations --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c30bad5db..ea1dac3ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.3.2) + addressable (2.3.6) arr-pm (0.0.7) cabin (> 0) aws-s3 (0.6.3) @@ -29,7 +29,7 @@ GEM simplecov (>= 0.7) term-ansicolor thor - crack (0.3.2) + crack (0.4.2) diff-lcs (1.2.5) docile (1.1.5) excon (0.39.4) @@ -73,9 +73,9 @@ GEM tins (~> 1.0) thor (0.19.1) tins (1.3.1) - webmock (1.9.0) - addressable (>= 2.2.7) - crack (>= 0.1.7) + webmock (1.18.0) + addressable (>= 2.3.6) + crack (>= 0.3.2) xml-simple (1.1.2) PLATFORMS From 9a374983435c66b97bb9e303b2b47d158db52d4b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 10:53:37 -0700 Subject: [PATCH 025/952] rspec 3 --- Gemfile | 2 +- Gemfile.lock | 22 ++++++++++++-------- spec/heroku/client/heroku_postgresql_spec.rb | 5 +++-- spec/heroku/command/pgbackups_spec.rb | 4 ++-- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 6f94b6cf0..04a0f9a9f 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ group :test do gem "fakefs" gem "jruby-openssl", :platform => :jruby gem "json" - gem "rspec", '~> 2.99' + gem "rspec" gem "sqlite3" gem "webmock" gem "coveralls", :require => false diff --git a/Gemfile.lock b/Gemfile.lock index ea1dac3ca..d9c53f2fd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,14 +53,18 @@ GEM rest-client (1.6.7) mime-types (>= 1.16) rr (1.0.4) - rspec (2.99.0) - rspec-core (~> 2.99.0) - rspec-expectations (~> 2.99.0) - rspec-mocks (~> 2.99.0) - rspec-core (2.99.1) - rspec-expectations (2.99.2) - diff-lcs (>= 1.1.3, < 2.0) - rspec-mocks (2.99.2) + rspec (3.0.0) + rspec-core (~> 3.0.0) + rspec-expectations (~> 3.0.0) + rspec-mocks (~> 3.0.0) + rspec-core (3.0.4) + rspec-support (~> 3.0.0) + rspec-expectations (3.0.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.0.0) + rspec-mocks (3.0.4) + rspec-support (~> 3.0.0) + rspec-support (3.0.4) rubyzip (0.9.9) simplecov (0.9.0) docile (~> 1.1.0) @@ -92,7 +96,7 @@ DEPENDENCIES json rake (>= 0.8.7) rr (~> 1.0.2) - rspec (~> 2.99) + rspec rubyzip sqlite3 webmock diff --git a/spec/heroku/client/heroku_postgresql_spec.rb b/spec/heroku/client/heroku_postgresql_spec.rb index 65a36a3a5..9dba3c04f 100644 --- a/spec/heroku/client/heroku_postgresql_spec.rb +++ b/spec/heroku/client/heroku_postgresql_spec.rb @@ -6,7 +6,8 @@ include Heroku::Helpers before do - Heroku::Auth.stub :user => 'user@example.com', :password => 'apitoken' + allow(Heroku::Auth).to receive(:user).and_return('user@example.com') + allow(Heroku::Auth).to receive(:password).and_return('apitoken') end let(:attachment) { double('attachment', :resource_name => 'something-something-42', :starter_plan? => false) } @@ -14,7 +15,7 @@ describe 'api choosing' do it "sends an ingress request to the client for production plans" do - attachment.stub :starter_plan? => false + expect(attachment).to receive(:starter_plan?).and_return(false) host = 'postgres-api.heroku.com' url = "https://user@example.com:apitoken@#{host}/client/v11/databases/#{attachment.resource_name}/ingress" diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index 86a613e50..5db07aa4d 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -283,8 +283,8 @@ def stub_failed_capture(log) context "on errors" do before(:each) do - @pgbackups_client.stub(:get_latest_backup => {"to_url" => "s3://bucket/user/bXXX.dump"} ) - @pgbackups.stub(:confirm_command => true) + allow(@pgbackups_client).to receive(:get_latest_backup).and_return("to_url" => "s3://bucket/user/bXXX.dump") + allow(@pgbackups).to receive(:confirm_command).and_return(true) end def stub_error_backup_with_log(log) From 19a8a1b38b3593098d1ca963be48e3d41fa2d476 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 11:05:36 -0700 Subject: [PATCH 026/952] travis fix --- .travis.yml | 2 +- Gemfile.lock | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 34ef14d6c..997a5d83f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,4 @@ rvm: - 2.0.0 - 2.1.2 -script: bundle exec rspec -bfs spec +script: bundle exec rspec spec --color --format documentation diff --git a/Gemfile.lock b/Gemfile.lock index d9c53f2fd..cee0c7018 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,6 +30,7 @@ GEM term-ansicolor thor crack (0.4.2) + safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) excon (0.39.4) @@ -66,6 +67,7 @@ GEM rspec-support (~> 3.0.0) rspec-support (3.0.4) rubyzip (0.9.9) + safe_yaml (1.0.3) simplecov (0.9.0) docile (~> 1.1.0) multi_json From f0969aecc5266dc612816ad78c2065387879ae15 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 15:41:26 -0700 Subject: [PATCH 027/952] trimmed unused gems and upgraded dev/test gems --- Gemfile | 15 +++------------ Gemfile.lock | 38 ++++++++++---------------------------- heroku.gemspec | 2 +- 3 files changed, 14 insertions(+), 41 deletions(-) diff --git a/Gemfile b/Gemfile index 04a0f9a9f..7025e9ec8 100644 --- a/Gemfile +++ b/Gemfile @@ -3,22 +3,13 @@ source "https://rubygems.org" gemspec group :development, :test do - gem "rake", ">= 0.8.7" - gem "rr", "~> 1.0.2" -end - -group :development do + gem "rake" + gem "rr" gem "aws-s3" - gem "fpm" - gem "rubyzip" -end - -group :test do + gem "mime-types", "< 2.0" gem "fakefs" - gem "jruby-openssl", :platform => :jruby gem "json" gem "rspec" - gem "sqlite3" gem "webmock" gem "coveralls", :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index cee0c7018..3990d4919 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,23 +6,16 @@ PATH launchy (>= 0.3.2) netrc (~> 0.7.7) rest-client (= 1.6.7) - rubyzip + rubyzip (= 0.9.9) GEM remote: https://rubygems.org/ specs: addressable (2.3.6) - arr-pm (0.0.7) - cabin (> 0) aws-s3 (0.6.3) builder - mime-types xml-simple - backports (2.3.0) - builder (3.1.4) - cabin (0.4.4) - json - clamp (0.5.0) + builder (3.2.2) coveralls (0.7.0) multi_json (~> 1.3) rest-client @@ -34,26 +27,20 @@ GEM diff-lcs (1.2.5) docile (1.1.5) excon (0.39.4) - fakefs (0.4.2) - fpm (0.4.6) - arr-pm (~> 0.0.7) - backports (= 2.3.0) - cabin (~> 0.4.3) - clamp - json + fakefs (0.5.2) heroku-api (0.3.19) excon (~> 0.38) multi_json (~> 1.8) json (1.7.7) launchy (2.4.2) addressable (~> 2.3) - mime-types (1.21) + mime-types (1.25.1) multi_json (1.10.1) netrc (0.7.7) - rake (10.0.3) + rake (10.3.2) rest-client (1.6.7) mime-types (>= 1.16) - rr (1.0.4) + rr (1.1.2) rspec (3.0.0) rspec-core (~> 3.0.0) rspec-expectations (~> 3.0.0) @@ -73,8 +60,6 @@ GEM multi_json simplecov-html (~> 0.8.0) simplecov-html (0.8.0) - sqlite3 (1.3.7) - sqlite3 (1.3.7-x86-mingw32) term-ansicolor (1.3.0) tins (~> 1.0) thor (0.19.1) @@ -82,7 +67,7 @@ GEM webmock (1.18.0) addressable (>= 2.3.6) crack (>= 0.3.2) - xml-simple (1.1.2) + xml-simple (1.1.4) PLATFORMS ruby @@ -92,13 +77,10 @@ DEPENDENCIES aws-s3 coveralls fakefs - fpm heroku! - jruby-openssl json - rake (>= 0.8.7) - rr (~> 1.0.2) + mime-types (< 2.0) + rake + rr rspec - rubyzip - sqlite3 webmock diff --git a/heroku.gemspec b/heroku.gemspec index ddf4a582d..668e83b04 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |gem| gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", "~> 0.7.7" gem.add_dependency "rest-client", "= 1.6.7" - gem.add_dependency "rubyzip" + gem.add_dependency "rubyzip", "= 0.9.9" end From 18973adbe6f201d2253690d9852a34034d5bc994 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 14 Aug 2014 16:21:03 -0700 Subject: [PATCH 028/952] Fail when both arguments to pgbackups:transfer resolve to same DB --- lib/heroku/command/pgbackups.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/heroku/command/pgbackups.rb b/lib/heroku/command/pgbackups.rb index 9754d7ac5..839e235d2 100644 --- a/lib/heroku/command/pgbackups.rb +++ b/lib/heroku/command/pgbackups.rb @@ -219,6 +219,10 @@ def transfer validate_arguments! + if from.url == to.url + error("source and target database are the same") + end + opts = {} verify_app = to.app || app if confirm_command(verify_app, "WARNING: Destructive Action\nTransfering data from #{from.name} to #{to.name}") From 1f8f8a14758c34b90c55de88884ff23b68a78ca9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 15:30:30 -0700 Subject: [PATCH 029/952] Convert specs to RSpec 3.0.4 syntax with Transpec This conversion is done by Transpec 2.3.6 with the following command: transpec * 19 conversions from: == expected to: eq(expected) * 19 conversions from: obj.should to: expect(obj).to * 2 conversions from: obj.stub(:message) to: allow(obj).to receive(:message) * 1 conversion from: obj.stub(:message => value) to: allow(obj).to receive_messages(:message => value) For more details: https://github.com/yujinakayama/transpec#supported-conversions --- spec/heroku/client/heroku_postgresql_spec.rb | 2 +- spec/heroku/updater_spec.rb | 42 ++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/spec/heroku/client/heroku_postgresql_spec.rb b/spec/heroku/client/heroku_postgresql_spec.rb index 9dba3c04f..94b575c5c 100644 --- a/spec/heroku/client/heroku_postgresql_spec.rb +++ b/spec/heroku/client/heroku_postgresql_spec.rb @@ -30,7 +30,7 @@ end it "sends an ingress request to the client for production plans" do - attachment.stub :starter_plan? => true + allow(attachment).to receive_messages :starter_plan? => true host = 'postgres-starter-api.heroku.com' url = "https://user@example.com:apitoken@#{host}/client/v11/databases/#{attachment.resource_name}/ingress" diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index 68069b2ba..259dacbbc 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -13,34 +13,34 @@ module Heroku describe('::compare_versions') do it 'calculates compare_versions' do - subject.compare_versions('1.1.1', '1.1.1').should == 0 + expect(subject.compare_versions('1.1.1', '1.1.1')).to eq(0) - subject.compare_versions('2.1.1', '1.1.1').should == 1 - subject.compare_versions('1.1.1', '2.1.1').should == -1 + expect(subject.compare_versions('2.1.1', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '2.1.1')).to eq(-1) - subject.compare_versions('1.2.1', '1.1.1').should == 1 - subject.compare_versions('1.1.1', '1.2.1').should == -1 + expect(subject.compare_versions('1.2.1', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '1.2.1')).to eq(-1) - subject.compare_versions('1.1.2', '1.1.1').should == 1 - subject.compare_versions('1.1.1', '1.1.2').should == -1 + expect(subject.compare_versions('1.1.2', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '1.1.2')).to eq(-1) - subject.compare_versions('2.1.1', '1.2.1').should == 1 - subject.compare_versions('1.2.1', '2.1.1').should == -1 + expect(subject.compare_versions('2.1.1', '1.2.1')).to eq(1) + expect(subject.compare_versions('1.2.1', '2.1.1')).to eq(-1) - subject.compare_versions('2.1.1', '1.1.2').should == 1 - subject.compare_versions('1.1.2', '2.1.1').should == -1 + expect(subject.compare_versions('2.1.1', '1.1.2')).to eq(1) + expect(subject.compare_versions('1.1.2', '2.1.1')).to eq(-1) - subject.compare_versions('1.2.4', '1.2.3').should == 1 - subject.compare_versions('1.2.3', '1.2.4').should == -1 + expect(subject.compare_versions('1.2.4', '1.2.3')).to eq(1) + expect(subject.compare_versions('1.2.3', '1.2.4')).to eq(-1) - subject.compare_versions('1.2.1', '1.2' ).should == 1 - subject.compare_versions('1.2', '1.2.1').should == -1 + expect(subject.compare_versions('1.2.1', '1.2' )).to eq(1) + expect(subject.compare_versions('1.2', '1.2.1')).to eq(-1) - subject.compare_versions('1.1.1.pre1', '1.1.1').should == 1 - subject.compare_versions('1.1.1', '1.1.1.pre1').should == -1 + expect(subject.compare_versions('1.1.1.pre1', '1.1.1')).to eq(1) + expect(subject.compare_versions('1.1.1', '1.1.1.pre1')).to eq(-1) - subject.compare_versions('1.1.1.pre2', '1.1.1.pre1').should == 1 - subject.compare_versions('1.1.1.pre1', '1.1.1.pre2').should == -1 + expect(subject.compare_versions('1.1.1.pre2', '1.1.1.pre1')).to eq(1) + expect(subject.compare_versions('1.1.1.pre1', '1.1.1.pre2')).to eq(-1) end end @@ -52,13 +52,13 @@ module Heroku shared_context 'with local version at 3.9.6' do before do - subject.stub(:latest_local_version).and_return('3.9.6') + allow(subject).to receive(:latest_local_version).and_return('3.9.6') end end shared_context 'with local version at 3.9.7' do before do - subject.stub(:latest_local_version).and_return('3.9.7') + allow(subject).to receive(:latest_local_version).and_return('3.9.7') end end From 7e1a37be8580cd88470acc2a0047dc0ee657b9f8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Aug 2014 16:06:43 -0700 Subject: [PATCH 030/952] prevent uploading to beta releases older than the current release --- lib/heroku/updater.rb | 7 ++++++- spec/heroku/updater_spec.rb | 40 ++++++++++++++++++------------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 94563e668..5ff50f675 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -110,11 +110,16 @@ def self.update(prerelease) extract_zip(zip_filename, download_dir) FileUtils.rm_f zip_filename + version = client_version_from_path(download_dir) + + # do not replace beta version if it is old + return if version < latest_local_version + FileUtils.rm_rf updated_client_path FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path - client_version_from_path(download_dir) + version end end end diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index 259dacbbc..8599664f1 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -44,26 +44,10 @@ module Heroku end end - shared_context 'with released version at 3.9.7' do + describe '::update' do before do Excon.stub({:host => 'assets.heroku.com', :path => '/heroku-client/VERSION'}, {:body => "3.9.7\n"}) end - end - - shared_context 'with local version at 3.9.6' do - before do - allow(subject).to receive(:latest_local_version).and_return('3.9.6') - end - end - - shared_context 'with local version at 3.9.7' do - before do - allow(subject).to receive(:latest_local_version).and_return('3.9.7') - end - end - - describe '::update' do - include_context 'with released version at 3.9.7' describe 'non-beta' do before do @@ -74,7 +58,9 @@ module Heroku end context 'with no update available' do - include_context 'with local version at 3.9.7' + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.7') + end it 'does not update' do expect(subject.update(false)).to be_nil @@ -82,7 +68,9 @@ module Heroku end context 'with an update available' do - include_context 'with local version at 3.9.6' + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.6') + end it 'updates' do expect(subject.update(false)).to eq('3.9.7') @@ -97,12 +85,24 @@ module Heroku end context 'with no update available' do - include_context 'with local version at 3.9.7' + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.7') + end it 'still updates' do expect(subject.update(true)).to eq('3.9.7') end end + + context 'with a beta older than what we have' do + before do + allow(subject).to receive(:latest_local_version).and_return('3.9.8') + end + + it 'does not update' do + expect(subject.update(true)).to be_nil + end + end end end end From 58b953281ef03009d55efd7f121a40bb0a16a00b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 18 Aug 2014 08:54:19 -0700 Subject: [PATCH 031/952] upgrade json gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3990d4919..492cfa042 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,7 +31,7 @@ GEM heroku-api (0.3.19) excon (~> 0.38) multi_json (~> 1.8) - json (1.7.7) + json (1.8.1) launchy (2.4.2) addressable (~> 2.3) mime-types (1.25.1) From 20a11b066e4a751afd40293d23c1b33577e41bf8 Mon Sep 17 00:00:00 2001 From: Mark Fine Date: Fri, 10 Jan 2014 22:41:25 -0500 Subject: [PATCH 032/952] remove price tier from info. --- lib/heroku/command/apps.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 6f10db1e2..6e5ce8deb 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -178,10 +178,6 @@ def info data["Web URL"] = app_data["web_url"] - if app_data["tier"] - data["Tier"] = app_data["tier"].capitalize - end - styled_hash(data) end end From 412e90eda1e7fab9d39dd1cd9518c90e0df06bef Mon Sep 17 00:00:00 2001 From: geemus Date: Tue, 19 Aug 2014 15:12:21 -0500 Subject: [PATCH 033/952] add help header for two factor topic --- lib/heroku/command/two_factor.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb index c2c1a6efd..447c89dcb 100644 --- a/lib/heroku/command/two_factor.rb +++ b/lib/heroku/command/two_factor.rb @@ -1,5 +1,7 @@ require "heroku/command/base" +# manage two factor settings for account +# module Heroku::Command class TwoFactor < BaseWithApp # 2fa From 89293efcfdd1147fdf197edaa1768779693de911 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 20 Aug 2014 10:32:03 -0700 Subject: [PATCH 034/952] removed lazy-requiring of rest-client and heroku-api --- lib/heroku/auth.rb | 27 +++++++++++--------------- lib/heroku/cli.rb | 8 ++------ lib/heroku/client.rb | 3 --- lib/heroku/client/heroku_postgresql.rb | 1 - lib/heroku/client/organizations.rb | 1 - lib/heroku/client/pgbackups.rb | 1 - lib/heroku/command.rb | 11 ++--------- spec/spec_helper.rb | 5 ----- 8 files changed, 15 insertions(+), 42 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index d759a9188..5ec04b2d9 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -13,7 +13,6 @@ class << self def api @api ||= begin - require("heroku-api") api = Heroku::API.new(default_params.merge(:api_key => password)) def api.request(params, &block) @@ -75,7 +74,6 @@ def password # :nodoc: end def api_key(user = get_credentials[0], password = get_credentials[1]) - require("heroku-api") api = Heroku::API.new(default_params) api.post_login(user, password).body["api_key"] rescue Heroku::API::Errors::Unauthorized => e @@ -240,22 +238,19 @@ def ask_for_password end def ask_for_and_save_credentials - require("heroku-api") # for the errors - begin - @credentials = ask_for_credentials - write_credentials - check - rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e - delete_credentials - display "Authentication failed." - retry if retry_login? - exit 1 - rescue Exception => e - delete_credentials - raise e - end + @credentials = ask_for_credentials + write_credentials + check check_for_associated_ssh_key unless Heroku::Command.current_command == "keys:add" @credentials + rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e + delete_credentials + display "Authentication failed." + retry if retry_login? + exit 1 + rescue Exception => e + delete_credentials + raise e end def check_for_associated_ssh_key diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index cb0a734d8..36389c756 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -4,12 +4,8 @@ require "heroku" require "heroku/command" require "heroku/helpers" - -# workaround for rescue/reraise to define errors in command.rb failing in 1.8.6 -if RUBY_VERSION =~ /^1.8.6/ - require('heroku-api') - require('rest_client') -end +require 'rest_client' +require 'heroku-api' begin # attempt to load the JSON parser bundled with ruby for multi_json diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 18594ac0a..2ff6358d9 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -32,7 +32,6 @@ def self.gem_version_string attr_accessor :host, :user, :password def initialize(user, password, host=Heroku::Auth.host) - require 'rest_client' @user = user @password = password @host = host @@ -349,7 +348,6 @@ class Service attr_accessor :attached def initialize(client, app) - require 'rest_client' @client = client @app = app end @@ -435,7 +433,6 @@ class AppCrashed < RuntimeError; end # support for console sessions class ConsoleSession def initialize(id, app, client) - require 'rest_client' @id = id; @app = app; @client = client end def run(cmd) diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index 07038e70d..c712afbbb 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -18,7 +18,6 @@ def self.headers attr_reader :attachment def initialize(attachment) @attachment = attachment - require 'rest_client' end def heroku_postgresql_host diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 819ca9c3e..a2af72797 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -1,4 +1,3 @@ -require 'heroku-api' require "heroku/client" class Heroku::Client::Organizations diff --git a/lib/heroku/client/pgbackups.rb b/lib/heroku/client/pgbackups.rb index be129f531..a031daa9d 100644 --- a/lib/heroku/client/pgbackups.rb +++ b/lib/heroku/client/pgbackups.rb @@ -5,7 +5,6 @@ class Heroku::Client::Pgbackups include Heroku::Helpers def initialize(uri) - require 'rest_client' @uri = URI.parse(uri) end diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index f944955e9..c92c4c840 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -213,15 +213,8 @@ def self.prepare_run(cmd, args=[]) end def self.run(cmd, arguments=[]) - begin - object, method = prepare_run(cmd, arguments.dup) - object.send(method) - rescue Interrupt, StandardError, SystemExit => error - # load likely error classes, as they may not be loaded yet due to defered loads - require 'heroku-api' - require 'rest_client' - raise(error) - end + object, method = prepare_run(cmd, arguments.dup) + object.send(method) rescue Heroku::API::Errors::Unauthorized, RestClient::Unauthorized => e retry_login = handle_auth_error(e) retry if retry_login diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 490204e75..04956a28d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,11 +7,6 @@ require "excon" -# ensure these are around for errors -# as their require is generally deferred -require "heroku-api" -require "rest_client" - require "heroku/cli" require "rspec" require "rr" From ca755ae7212af395578623be3df59d1ac071a0b6 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Thu, 21 Aug 2014 14:58:37 -0700 Subject: [PATCH 035/952] Send deploy type and source metadata for forks --- lib/heroku/api/releases_v3.rb | 17 +++++++++++------ lib/heroku/command/fork.rb | 6 +++++- spec/heroku/command/fork_spec.rb | 6 +++++- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/heroku/api/releases_v3.rb b/lib/heroku/api/releases_v3.rb index 41a4d0815..632f168f7 100644 --- a/lib/heroku/api/releases_v3.rb +++ b/lib/heroku/api/releases_v3.rb @@ -11,15 +11,20 @@ def get_releases_v3(app, range=nil) ) end - def post_release_v3(app, slug_id, description=nil) + def post_release_v3(app, slug_id, opts={}) + headers = { + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Content-Type' => 'application/json' + } + headers.merge!('Heroku-Deploy-Type' => opts[:deploy_type]) if opts[:deploy_type] + headers.merge!('Heroku-Deploy-Source' => opts[:deploy_source]) if opts[:deploy_source] + body = { 'slug' => slug_id } - body.merge!('description' => description) if description + body.merge!('description' => opts[:description]) if opts[:description] + request( :expects => 201, - :headers => { - 'Accept' => 'application/vnd.heroku+json; version=3', - 'Content-Type' => 'application/json' - }, + :headers => headers, :method => :post, :path => "/apps/#{app}/releases", :body => Heroku::Helpers.json_encode(body) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index f92bb2d29..f9cd8281a 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -113,7 +113,11 @@ def copy_slug(from, to) raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? from_slug = from_releases.first.fetch('slug', {}) raise Heroku::Command::CommandFailed.new("No slug on #{from}") unless from_slug - api.post_release_v3(to, from_slug["id"], "Forked from #{from}") + api.post_release_v3(to, + from_slug["id"], + :description => "Forked from #{from}", + :deploy_type => "fork", + :deploy_source => from) end def check_for_pgbackups!(app) diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb index 85486faf6..6bb13a8e1 100644 --- a/spec/heroku/command/fork_spec.rb +++ b/spec/heroku/command/fork_spec.rb @@ -50,7 +50,11 @@ module Heroku::Command it "copies slug" do expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original - expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", "SLUG_ID", "Forked from example").and_call_original + expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", + "SLUG_ID", + :description => "Forked from example", + :deploy_type => "fork", + :deploy_source => "example").and_call_original execute("fork example-fork") end From ba37eafb090f1773e5de62b47918963d69a6c397 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 20 Aug 2014 11:52:21 -0700 Subject: [PATCH 036/952] appveyor --- Gemfile | 1 + Gemfile.lock | 2 ++ README.md | 1 + appveyor.yml | 16 ++++++++++++++++ 4 files changed, 20 insertions(+) create mode 100644 appveyor.yml diff --git a/Gemfile b/Gemfile index 7025e9ec8..e016c9c38 100644 --- a/Gemfile +++ b/Gemfile @@ -12,4 +12,5 @@ group :development, :test do gem "rspec" gem "webmock" gem "coveralls", :require => false + gem "ocra", :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index 492cfa042..ec4931804 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,6 +37,7 @@ GEM mime-types (1.25.1) multi_json (1.10.1) netrc (0.7.7) + ocra (1.3.2) rake (10.3.2) rest-client (1.6.7) mime-types (>= 1.16) @@ -80,6 +81,7 @@ DEPENDENCIES heroku! json mime-types (< 2.0) + ocra rake rr rspec diff --git a/README.md b/README.md index 0c29dfabd..10bd0ba75 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ For more about Heroku see . To get started see [![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) +[![Build status](https://ci.appveyor.com/api/projects/status/kv0r2s5eyckpanhr/branch/master)](https://ci.appveyor.com/project/dickeyxxx/heroku/branch/master) [![Coverage Status](https://img.shields.io/coveralls/heroku/heroku.svg)](https://coveralls.io/r/heroku/heroku?branch=master) [![Dependency Status](https://gemnasium.com/heroku/heroku.svg)](https://gemnasium.com/heroku/heroku) diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..679e2dd7b --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,16 @@ +version: "{build}" +branches: + only: + - master +clone_depth: 1 +install: + - ruby --version + - bundle install -j4 +build_script: + - ocra bin\heroku +test_script: + - heroku.exe help + - heroku.exe status +artifacts: + - path: heroku.exe + name: heroku.exe From e2064efe0de48ea4646376db622f7fd264815639 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 18:35:05 -0700 Subject: [PATCH 037/952] hipchat notifications for appveyor --- appveyor.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 679e2dd7b..8b6680dff 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,3 +14,9 @@ test_script: artifacts: - path: heroku.exe name: heroku.exe + +notifications: + - provider: HipChat + auth_token: + secure: MRgnRhuTl4lmIvyZ58dhKDB+g18Lln7PVdMprSYPG3gPzkDM9FnXrucEUtGfV3qE + room: CLI From 32fafaef1b74a3a5d71ec1de95a063319dd33ac7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 18:28:46 -0700 Subject: [PATCH 038/952] changed notifications to hipchat and added bundler caching --- .travis.yml | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 997a5d83f..cac6bad67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,19 @@ -before_script: - - git config --global user.email "bot@heroku.com" - - git config --global user.name "Heroku Bot (Travis CI)" - -bundler_args: --without development - language: ruby - -notifications: - email: false - webhooks: - on_success: always - on_failure: always - urls: - - http://dx-helper.herokuapp.com/travis - rvm: - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 - 2.1.2 +cache: bundler + +before_script: + - git config --global user.email "bot@heroku.com" + - git config --global user.name "Heroku Bot (Travis CI)" script: bundle exec rspec spec --color --format documentation + +notifications: + hipchat: + rooms: + secure: vkWV7oGjSxVDPXemj5UXVxZR2/pWQkNaz2I3HUJ7OO40TL08X5yZKJVFwkU0AGqJ7EOck/Jtsxc01t70G+z6EcjcI6471GygmI15zbv8IkxIazZkUsybUR1NglUfeuJzjK1JEArK6hJ/n75DyR09pl+ZDkE8fuyrG+TC/nusd08= From 97e7b2aeecabf216a30398193c177858e586504b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 18:49:14 -0700 Subject: [PATCH 039/952] travis rubygems deploys on git tagging --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index cac6bad67..746fde1d5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,3 +17,10 @@ notifications: hipchat: rooms: secure: vkWV7oGjSxVDPXemj5UXVxZR2/pWQkNaz2I3HUJ7OO40TL08X5yZKJVFwkU0AGqJ7EOck/Jtsxc01t70G+z6EcjcI6471GygmI15zbv8IkxIazZkUsybUR1NglUfeuJzjK1JEArK6hJ/n75DyR09pl+ZDkE8fuyrG+TC/nusd08= + +deploy: + provider: rubygems + on: + tags: true + api_key: + secure: ALsBCGGvdAiIEJR9zTzxumcgCaS5eqOs7Oee7e4SiDgHrT/DRSsFJBtNp9mJvQvHzW3FqSFZU7NO6tSRkwHGdGGw7pf/emjZ2ua0exuyCQ3LaCJBdwSQXl0GTMhhaMCCd2NYWJ+Fa3Q9jWWAdCfV8rqz5AX4ZG6fi3C2uubppVs= From 762e13ed8af4bcf310a31b2957345a43c36f327e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 18:51:08 -0700 Subject: [PATCH 040/952] dont push gem on rake release since travis should do that now --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index c381f9c28..df648ff65 100644 --- a/Rakefile +++ b/Rakefile @@ -172,7 +172,7 @@ CHANGELOG end desc("Release the latest version") -task "release" => ["gem:release", "tgz:release", "zip:release", "manifest:update"] do +task "release" => ["tgz:release", "zip:release", "manifest:update"] do puts("Released v#{version}") end From ca2cf72b623ba147bdc56b45376d76e4beca4276 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 18:54:58 -0700 Subject: [PATCH 041/952] dont send emails --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 746fde1d5..9494bbb12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ before_script: script: bundle exec rspec spec --color --format documentation notifications: + email: false hipchat: rooms: secure: vkWV7oGjSxVDPXemj5UXVxZR2/pWQkNaz2I3HUJ7OO40TL08X5yZKJVFwkU0AGqJ7EOck/Jtsxc01t70G+z6EcjcI6471GygmI15zbv8IkxIazZkUsybUR1NglUfeuJzjK1JEArK6hJ/n75DyR09pl+ZDkE8fuyrG+TC/nusd08= From 994432d449572544f44df0dce90c3ac88ecd1249 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 19:03:31 -0700 Subject: [PATCH 042/952] 3.10.1 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a045886b4..aee75b1c3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.10.2 2014-08-21 +================= +Removed lazy-loading of heroku-api and rest_client (was swallowing errors) +Fail fast for issue with pgbackups:transfer +Removed price tier from info +Help info for two factor topic +Send deploy type and source metadata for forks + 3.10.1 2014-08-14 ================= No changes, just verifying new release code is in order diff --git a/Gemfile.lock b/Gemfile.lock index ec4931804..47576c3e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.1) + heroku (3.10.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9278039e3..43f42b35c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.1" + VERSION = "3.10.2" end From 168b34f699fed9f8e4a8de403b0b341edef54faf Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Thu, 21 Aug 2014 19:06:57 -0700 Subject: [PATCH 043/952] Record source app id as fork deploy source --- lib/heroku/command/fork.rb | 8 +++++--- spec/heroku/command/fork_spec.rb | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index f9cd8281a..b38d2161e 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -42,7 +42,7 @@ def index end action("Copying slug") do - copy_slug(from, to) + copy_slug(from_info, to_info) end from_config = api.get_config_vars(from).body @@ -108,7 +108,9 @@ def index private - def copy_slug(from, to) + def copy_slug(from_info, to_info) + from = from_info["name"] + to = to_info["name"] from_releases = api.get_releases_v3(from, 'version ..; order=desc,max=1;').body raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? from_slug = from_releases.first.fetch('slug', {}) @@ -117,7 +119,7 @@ def copy_slug(from, to) from_slug["id"], :description => "Forked from #{from}", :deploy_type => "fork", - :deploy_source => from) + :deploy_source => from_info["id"]) end def check_for_pgbackups!(app) diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb index 6bb13a8e1..3d2adb050 100644 --- a/spec/heroku/command/fork_spec.rb +++ b/spec/heroku/command/fork_spec.rb @@ -49,12 +49,13 @@ module Heroku::Command end it "copies slug" do + from_info = api.get_app("example").body expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", "SLUG_ID", :description => "Forked from example", :deploy_type => "fork", - :deploy_source => "example").and_call_original + :deploy_source => from_info["id"]).and_call_original execute("fork example-fork") end From 99c50f9159965fa760e56c338778b4aaab3b1682 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 21 Aug 2014 19:13:32 -0700 Subject: [PATCH 044/952] 3.10.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index aee75b1c3..8bd86f8a7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.10.3 2014-08-21 +================= +Fixed minor issue with recording fork source + 3.10.2 2014-08-21 ================= Removed lazy-loading of heroku-api and rest_client (was swallowing errors) diff --git a/Gemfile.lock b/Gemfile.lock index 47576c3e6..cef209be8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.2) + heroku (3.10.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 43f42b35c..bab68ba11 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.2" + VERSION = "3.10.3" end From f9ae516eb28147a5c74d32de91e068081f1cad68 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 22 Aug 2014 10:08:16 -0700 Subject: [PATCH 045/952] upgrade gems (notably excon) --- Gemfile.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cef209be8..da4854b3c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,9 +14,10 @@ GEM addressable (2.3.6) aws-s3 (0.6.3) builder + mime-types xml-simple builder (3.2.2) - coveralls (0.7.0) + coveralls (0.7.1) multi_json (~> 1.3) rest-client simplecov (>= 0.7) @@ -26,7 +27,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.39.4) + excon (0.39.5) fakefs (0.5.2) heroku-api (0.3.19) excon (~> 0.38) @@ -64,7 +65,7 @@ GEM term-ansicolor (1.3.0) tins (~> 1.0) thor (0.19.1) - tins (1.3.1) + tins (1.3.2) webmock (1.18.0) addressable (>= 2.3.6) crack (>= 0.3.2) From e48c2fab000c04e7ceab154924878db2bc104038 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 22 Aug 2014 11:09:36 -0700 Subject: [PATCH 046/952] get rid of travis notifications (way too many messages) --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9494bbb12..4e70d1565 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,9 +15,6 @@ script: bundle exec rspec spec --color --format documentation notifications: email: false - hipchat: - rooms: - secure: vkWV7oGjSxVDPXemj5UXVxZR2/pWQkNaz2I3HUJ7OO40TL08X5yZKJVFwkU0AGqJ7EOck/Jtsxc01t70G+z6EcjcI6471GygmI15zbv8IkxIazZkUsybUR1NglUfeuJzjK1JEArK6hJ/n75DyR09pl+ZDkE8fuyrG+TC/nusd08= deploy: provider: rubygems From 1e9e43cc47e46a2394cf75ae08819cd03bbc2e32 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 22 Aug 2014 12:00:36 -0700 Subject: [PATCH 047/952] v3.10.4 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8bd86f8a7..11b04c2fc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.10.4 2014-08-22 +================= +Upgraded excon to 0.39.5 + 3.10.3 2014-08-21 ================= Fixed minor issue with recording fork source diff --git a/Gemfile.lock b/Gemfile.lock index da4854b3c..01620d7d1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.3) + heroku (3.10.4) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index bab68ba11..cf4ac3516 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.3" + VERSION = "3.10.4" end From c07e394bea6c147ffb05efb77849d142dd851177 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 26 Aug 2014 13:34:36 -0700 Subject: [PATCH 048/952] added ocra script --- appveyor.yml | 10 +++++----- bin/heroku-ocra | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) create mode 100644 bin/heroku-ocra diff --git a/appveyor.yml b/appveyor.yml index 8b6680dff..a6509027a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,13 +7,13 @@ install: - ruby --version - bundle install -j4 build_script: - - ocra bin\heroku + - ocra bin\heroku-ocra data\cacert.pem test_script: - - heroku.exe help - - heroku.exe status + - heroku-ocra.exe help + - heroku-ocra.exe status artifacts: - - path: heroku.exe - name: heroku.exe + - path: heroku-ocra.exe + name: heroku-ocra.exe notifications: - provider: HipChat diff --git a/bin/heroku-ocra b/bin/heroku-ocra new file mode 100644 index 000000000..d464fa12a --- /dev/null +++ b/bin/heroku-ocra @@ -0,0 +1,21 @@ +#!/usr/bin/env ruby +# encoding: UTF-8 + +# resolve bin path, ignoring symlinks +require "pathname" +bin_file = Pathname.new(__FILE__).realpath + +# add self to libpath +$:.unshift File.expand_path("../../lib", bin_file) + +require "heroku/updater" +Heroku::Updater.disable("`heroku update` is only available from Heroku Toolbelt.\nDownload and install from https://toolbelt.heroku.com") + +# start up the CLI +require "heroku/cli" +Heroku.user_agent = "heroku-ocra/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" +Heroku::CLI.start(*ARGV) + +# require other dependencies ocra needs to include +require 'Win32API' +MultiJson.load('{"foo": "bar"}') # preps multi_json From d40f2a7cfe161f47efe54c7d4b8b07b61559f119 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 26 Aug 2014 13:39:32 -0700 Subject: [PATCH 049/952] removed hipchat notifications --- appveyor.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a6509027a..7cd92d822 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,9 +14,3 @@ test_script: artifacts: - path: heroku-ocra.exe name: heroku-ocra.exe - -notifications: - - provider: HipChat - auth_token: - secure: MRgnRhuTl4lmIvyZ58dhKDB+g18Lln7PVdMprSYPG3gPzkDM9FnXrucEUtGfV3qE - room: CLI From 759df4f807b60be603221c067c2698daec0aa8c1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 27 Aug 2014 15:54:50 -0700 Subject: [PATCH 050/952] v3.10.5 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 11b04c2fc..5b816658e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.10.5 2014-08-27 +================= +Ocra support for building standalone heroku-ocra.exe + 3.10.4 2014-08-22 ================= Upgraded excon to 0.39.5 diff --git a/Gemfile.lock b/Gemfile.lock index 01620d7d1..9e793ed12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.4) + heroku (3.10.5) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index cf4ac3516..fb3c4b924 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.4" + VERSION = "3.10.5" end From 023a8825bae621c0f992e333b1fa63b00c45ee77 Mon Sep 17 00:00:00 2001 From: Daniel Morrison Date: Wed, 3 Sep 2014 13:07:47 -0400 Subject: [PATCH 051/952] require rest-client in Client Fixes #1201 RestClient is not available if you use Client directly without CLI/ --- lib/heroku/client.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 2ff6358d9..60194b688 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -6,6 +6,7 @@ require 'heroku/helpers' require 'heroku/version' require 'heroku/client/ssl_endpoint' +require 'rest_client' # A Ruby class to call the Heroku REST API. You might use this if you want to # manage your Heroku apps from within a Ruby program, such as Capistrano. From a276639f3f62e3152b71272425e1901e2777dcdb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 4 Sep 2014 15:51:30 -0700 Subject: [PATCH 052/952] v3.10.6 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5b816658e..f5a321f00 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.10.6 2014-09-04 +================= +Added ssh-keygen shim for Windows + 3.10.5 2014-08-27 ================= Ocra support for building standalone heroku-ocra.exe diff --git a/Gemfile.lock b/Gemfile.lock index 9e793ed12..c1865f53f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.5) + heroku (3.10.6) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index fb3c4b924..e390d60ba 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.5" + VERSION = "3.10.6" end From da596d5efe63dafe6c947b6371920665e5bbd0ce Mon Sep 17 00:00:00 2001 From: Brandur Date: Wed, 10 Sep 2014 11:21:22 -0700 Subject: [PATCH 053/952] Support for paranoid operations on commands using `RestClient` Paranoid currently only works in Excon. This adds support to `RestClient` as well. --- lib/heroku/client.rb | 4 ++++ lib/heroku/command.rb | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 60194b688..7286713ad 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -614,6 +614,10 @@ def process(method, uri, extra_headers={}, payload=nil) headers = heroku_headers.merge(extra_headers) args = [method, payload, headers].compact + if Heroku::Auth.two_factor_code + headers.merge!("Heroku-Two-Factor-Code" => Heroku::Auth.two_factor_code) + end + resource_options = default_resource_options_for_uri(uri) begin diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index c92c4c840..e9e6ede75 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -252,7 +252,12 @@ def self.run(cmd, arguments=[]) rescue Heroku::API::Errors::ErrorWithResponse => e error extract_error(e.response.body) rescue RestClient::RequestFailed => e - error extract_error(e.http_body) + if e.response.code == 403 && e.response.headers.has_key?(:heroku_two_factor_required) + Heroku::Auth.ask_for_second_factor + retry + else + error extract_error(e.http_body) + end rescue CommandFailed => e error e.message rescue OptionParser::ParseError From 0fd8b4e55321aec04bf8bf28bbdc32e5fc7f92db Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 12 Sep 2014 13:34:01 -0700 Subject: [PATCH 054/952] Try to use ~/.ssh/id_rsa.pub as the public ssh key --- lib/heroku/auth.rb | 45 ++++++++++++++++++-------------- spec/heroku/auth_spec.rb | 20 ++++---------- spec/heroku/command/keys_spec.rb | 17 ++++-------- 3 files changed, 36 insertions(+), 46 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 5ec04b2d9..5387a5d22 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -255,25 +255,37 @@ def ask_for_and_save_credentials def check_for_associated_ssh_key if api.get_keys.body.empty? + display "Your Heroku account does not have a public ssh key uploaded." associate_or_generate_ssh_key end end def associate_or_generate_ssh_key - public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort - - case public_keys.length - when 0 then - display "Could not find an existing public key." + unless File.exists?("#{home_directory}/.ssh/id_rsa.pub") + display "Could not find an existing public key at ~/.ssh/id_rsa.pub" display "Would you like to generate one? [Yn] ", false - unless ask.strip.downcase == "n" + unless ask.strip.downcase =~ /^n/ display "Generating new SSH public key." - generate_ssh_key("id_rsa") + generate_ssh_key("#{home_directory}/.ssh/id_rsa") associate_key("#{home_directory}/.ssh/id_rsa.pub") + return end - when 1 then - display "Found existing public key: #{public_keys.first}" - associate_key(public_keys.first) + end + + chosen = ssh_prompt + associate_key(chosen) if chosen + end + + def ssh_prompt + public_keys = Dir.glob("#{home_directory}/.ssh/*.pub").sort + case public_keys.length + when 0 + error("No SSH keys found") + return nil + when 1 + display "Found an SSH public key at #{public_keys.first}" + display "Would you like to upload it to Heroku? [Yn] ", false + return ask.strip.downcase =~ /^n/ ? nil : public_keys.first else display "Found the following SSH public keys:" public_keys.each_with_index do |key, index| @@ -285,19 +297,14 @@ def associate_or_generate_ssh_key if choice == -1 || chosen.nil? error("Invalid choice") end - associate_key(chosen) + return chosen end end def generate_ssh_key(keyfile) - ssh_dir = File.join(home_directory, ".ssh") - unless File.exists?(ssh_dir) - FileUtils.mkdir_p ssh_dir - unless running_on_windows? - File.chmod(0700, ssh_dir) - end - end - output = `ssh-keygen -t rsa -N "" -f \"#{home_directory}/.ssh/#{keyfile}\" 2>&1` + ssh_dir = File.dirname(keyfile) + FileUtils.mkdir_p ssh_dir, :mode => 0700 + output = `ssh-keygen -t rsa -N "" -f \"#{keyfile}\" 2>&1` if ! $?.success? error("Could not generate key: #{output}") end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index 9da71e573..5e50b1614 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -185,6 +185,7 @@ module Heroku describe "automatic key uploading" do before(:each) do + allow(@cli).to receive(:home_directory).and_return(Heroku::Helpers.home_directory) FileUtils.mkdir_p("#{@cli.home_directory}/.ssh") allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") end @@ -216,31 +217,20 @@ module Heroku describe "with zero public keys" do it "should ask to generate a key" do expect(@cli).to receive(:ask).and_return("y") - expect(@cli).to receive(:generate_ssh_key).with("id_rsa") - expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") - @cli.check_for_associated_ssh_key - end - end - - describe "with one public key" do - before(:each) { FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa.pub") } - after(:each) { FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa.pub") } - - it "should upload the key" do + expect(@cli).to receive(:generate_ssh_key).with("#{@cli.home_directory}/.ssh/id_rsa") expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") @cli.check_for_associated_ssh_key end end describe "with many public keys" do - before(:each) do + before :each do FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa.pub") FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa2.pub") end - after(:each) do - FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa.pub") - FileUtils.rm("#{@cli.home_directory}/.ssh/id_rsa2.pub") + after :each do + FileUtils.rm_rf(@cli.home_directory) end it "should ask which key to upload" do diff --git a/spec/heroku/command/keys_spec.rb b/spec/heroku/command/keys_spec.rb index d479f17ab..bb4d5d210 100644 --- a/spec/heroku/command/keys_spec.rb +++ b/spec/heroku/command/keys_spec.rb @@ -7,27 +7,20 @@ module Heroku::Command before(:each) do stub_core + allow(Heroku::Auth).to receive(:home_directory).and_return(Heroku::Helpers.home_directory) end context("add") do - - after(:each) do - api.delete_key("pedro@heroku") - end - it "tries to find a key if no key filename is supplied" do expect(Heroku::Auth).to receive(:ask).and_return("y") - expect(Heroku::Auth).to receive(:generate_ssh_key) - expect(File).to receive(:exists?).with('.git').and_return(false) - expect(File).to receive(:exists?).with('/.ssh/id_rsa.pub').and_return(true) - expect(File).to receive(:read).with('/.ssh/id_rsa.pub').and_return(KEY) stderr, stdout = execute("keys:add") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Could not find an existing public key. +Could not find an existing public key at ~/.ssh/id_rsa.pub Would you like to generate one? [Yn] Generating new SSH public key. -Uploading SSH public key /.ssh/id_rsa.pub... done +Uploading SSH public key #{Heroku::Auth.home_directory}/.ssh/id_rsa.pub... done STDOUT + api.delete_key(`whoami`.strip + '@' + `hostname`.strip) end it "adds a key from a specified keyfile path" do @@ -39,8 +32,8 @@ module Heroku::Command expect(stdout).to eq <<-STDOUT Uploading SSH public key /my/key.pub... done STDOUT + api.delete_key("pedro@heroku") end - end context("index") do From e7ca7b03ae164868bc2a450fc39c2eab0f93d886 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 12 Sep 2014 15:56:22 -0700 Subject: [PATCH 055/952] v3.11.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f5a321f00..98f983081 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.11.0 2014-09-12 +================= +Attempt to push ~/.ssh/id_rsa SSH key instead of any one key +Always prompt for uploading SSH keys + 3.10.6 2014-09-04 ================= Added ssh-keygen shim for Windows diff --git a/Gemfile.lock b/Gemfile.lock index c1865f53f..a387a41a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.10.6) + heroku (3.11.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e390d60ba..39de090c9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.10.6" + VERSION = "3.11.0" end From e02e61f0efdc2dcaf7ea249995a05980deeee956 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 12 Sep 2014 18:04:28 -0700 Subject: [PATCH 056/952] bump --- lib/heroku/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 39de090c9..11eca788e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.11.0" + VERSION = "3.11.1" end From fda3b3cad22f4b7765613a497e1160e716d25498 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 12 Sep 2014 18:04:28 -0700 Subject: [PATCH 057/952] v3.11.1 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 98f983081..4fa2b1878 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -3.11.0 2014-09-12 +3.11.1 2014-09-12 ================= Attempt to push ~/.ssh/id_rsa SSH key instead of any one key Always prompt for uploading SSH keys From 49809d3d8ec8b173daff502a18f2374d360d8758 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 12 Sep 2014 18:05:22 -0700 Subject: [PATCH 058/952] v3.11.1 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a387a41a9..a99e8008d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.11.0) + heroku (3.11.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) From 09a7605c1d83a5743ee88c3e80e1dcbd7e4c6b0a Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 15 Sep 2014 09:01:53 -0700 Subject: [PATCH 059/952] Use server-side connection reset endpoint for pg:killall This lets users kill all their existing connections even when they have no remaining connections on their given database plan, preventing a chicken-and-egg problem in some situations. --- lib/heroku/client/heroku_postgresql.rb | 4 ++++ lib/heroku/command/pg.rb | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index c712afbbb..4f1957be6 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -57,6 +57,10 @@ def reset http_put "#{resource_name}/reset" end + def connection_reset + http_post "#{resource_name}/connection_reset" + end + def rotate_credentials http_post "#{resource_name}/credentials_rotation" end diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index a25e2faed..1f9246980 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -268,6 +268,14 @@ def kill # terminates ALL connections # def killall + db = args.first + attachment = generate_resolver.resolve(db, "DATABASE_URL") + client = hpg_client(attachment) + client.connection_reset + display "Connections terminated" + rescue StandardError + # fall back to original mechanism if calling the reset endpoint + # fails sql = %Q( SELECT pg_terminate_backend(#{pid_column}) FROM pg_stat_activity From dadc54edbb1e14a41103cbf84facdb4d450b8191 Mon Sep 17 00:00:00 2001 From: Brandur Date: Fri, 19 Sep 2014 13:53:47 -0700 Subject: [PATCH 060/952] Send all orgs-related requests to api.heroku.com As part of the eventual deprecation of the Manager API component, the main API will now start to respond to fulfill orgs-related requests. See also heroku/api#2837. --- lib/heroku/client/organizations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index a2af72797..dea83814d 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -230,7 +230,7 @@ def decompress_response!(response) end def manager_url - ENV['HEROKU_MANAGER_URL'] || "https://manager-api.heroku.com" + ENV['HEROKU_MANAGER_URL'] || "https://api.heroku.com" end end From 47bf3fc9d3fc6d360a23a61d726c04cc6fc4cb60 Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:16:45 -0700 Subject: [PATCH 061/952] filter our general availability features in labs display --- lib/heroku/command/labs.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/heroku/command/labs.rb b/lib/heroku/command/labs.rb index beb5a1da3..f9918c5e9 100644 --- a/lib/heroku/command/labs.rb +++ b/lib/heroku/command/labs.rb @@ -26,6 +26,9 @@ def index feature["kind"] == "user" end + # general availability features are managed via `settings`, not `labs` + app_features.reject! { |f| f["state"] == "general" } + display_app = app || "no app specified" styled_header "User Features (#{Heroku::Auth.user})" From fd700a371e7303c046cccf2846655e885ff13438 Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:18:49 -0700 Subject: [PATCH 062/952] initial settings command, copied from labs --- lib/heroku/command/settings.rb | 150 +++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 lib/heroku/command/settings.rb diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb new file mode 100644 index 000000000..ccb2502e9 --- /dev/null +++ b/lib/heroku/command/settings.rb @@ -0,0 +1,150 @@ +require "heroku/command/base" + +# manage optional features +# +class Heroku::Command::Settings < Heroku::Command::Base + + # labs + # + # list experimental features + # + #Example: + # + # === User Features (david@heroku.com) + # [+] dashboard Use Heroku Dashboard by default + # + # === App Features (glacial-retreat-5913) + # [ ] preboot Provide seamless web dyno deploys + # [ ] user-env-compile Add user config vars to the environment during slug compilation # $ heroku labs -a example + # + def index + validate_arguments! + + user_features, app_features = api.get_features(app).body.sort_by do |feature| + feature["name"] + end.partition do |feature| + feature["kind"] == "user" + end + + # general availability features are managed via `settings`, not `labs` + app_features.reject! { |f| f["state"] == "general" } + + display_app = app || "no app specified" + + styled_header "User Features (#{Heroku::Auth.user})" + display_features user_features + display + styled_header "App Features (#{display_app})" + display_features app_features + end + + alias_command "labs:list", "labs" + + # labs:info FEATURE + # + # displays additional information about FEATURE + # + #Example: + # + # $ heroku labs:info user_env_compile + # === user_env_compile + # Docs: http://devcenter.heroku.com/articles/labs-user-env-compile + # Summary: Add user config vars to the environment during slug compilation + # + def info + unless feature_name = shift_argument + error("Usage: heroku labs:info FEATURE\nMust specify FEATURE for info.") + end + validate_arguments! + + feature_data = api.get_feature(feature_name, app).body + styled_header(feature_data['name']) + styled_hash({ + 'Summary' => feature_data['summary'], + 'Docs' => feature_data['docs'] + }) + end + + # labs:disable FEATURE + # + # disables an experimental feature + # + #Example: + # + # $ heroku labs:disable ninja-power + # Disabling ninja-power feature for me@example.org... done + # + def disable + feature_name = shift_argument + error "Usage: heroku labs:disable FEATURE\nMust specify FEATURE to disable." unless feature_name + validate_arguments! + + feature = api.get_features(app).body.detect { |f| f["name"] == feature_name } + message = "Disabling #{feature_name} " + + error "No such feature: #{feature_name}" unless feature + + if feature["kind"] == "user" + message += "for #{Heroku::Auth.user}" + else + error "Must specify an app" unless app + message += "for #{app}" + end + + action message do + api.delete_feature feature_name, app + end + end + + # labs:enable FEATURE + # + # enables an experimental feature + # + #Example: + # + # $ heroku labs:enable ninja-power + # Enabling ninja-power feature for me@example.org... done + # + def enable + feature_name = shift_argument + error "Usage: heroku labs:enable FEATURE\nMust specify FEATURE to enable." unless feature_name + validate_arguments! + + feature = api.get_features.body.detect { |f| f["name"] == feature_name } + message = "Enabling #{feature_name} " + + error "No such feature: #{feature_name}" unless feature + + if feature["kind"] == "user" + message += "for #{Heroku::Auth.user}" + else + error "Must specify an app" unless app + message += "for #{app}" + end + + feature_data = action(message) do + api.post_feature(feature_name, app).body + end + + display "WARNING: This feature is experimental and may change or be removed without notice." + display "For more information see: #{feature_data["docs"]}" if feature_data["docs"] + end + +private + + # app is not required for these commands, so rescue if there is none + def app + super + rescue Heroku::Command::CommandFailed + nil + end + + def display_features(features) + longest_name = features.map { |f| f["name"].to_s.length }.sort.last + features.each do |feature| + toggle = feature["enabled"] ? "[+]" : "[ ]" + display "%s %-#{longest_name}s %s" % [ toggle, feature["name"], feature["summary"] ] + end + end + +end From 582c061dc902c98043880d75ec298ee215e721ce Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:38:52 -0700 Subject: [PATCH 063/952] FEATURE -> SETTING --- lib/heroku/command/settings.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index ccb2502e9..e210a9943 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -40,9 +40,9 @@ def index alias_command "labs:list", "labs" - # labs:info FEATURE + # labs:info SETTING # - # displays additional information about FEATURE + # displays additional information about SETTING # #Example: # @@ -53,7 +53,7 @@ def index # def info unless feature_name = shift_argument - error("Usage: heroku labs:info FEATURE\nMust specify FEATURE for info.") + error("Usage: heroku labs:info SETTING\nMust specify SETTING for info.") end validate_arguments! @@ -65,7 +65,7 @@ def info }) end - # labs:disable FEATURE + # labs:disable SETTING # # disables an experimental feature # @@ -76,7 +76,7 @@ def info # def disable feature_name = shift_argument - error "Usage: heroku labs:disable FEATURE\nMust specify FEATURE to disable." unless feature_name + error "Usage: heroku labs:disable SETTING\nMust specify SETTING to disable." unless feature_name validate_arguments! feature = api.get_features(app).body.detect { |f| f["name"] == feature_name } @@ -96,7 +96,7 @@ def disable end end - # labs:enable FEATURE + # labs:enable SETTING # # enables an experimental feature # @@ -107,7 +107,7 @@ def disable # def enable feature_name = shift_argument - error "Usage: heroku labs:enable FEATURE\nMust specify FEATURE to enable." unless feature_name + error "Usage: heroku labs:enable SETTING\nMust specify SETTING to enable." unless feature_name validate_arguments! feature = api.get_features.body.detect { |f| f["name"] == feature_name } From 717a97934a9c347a34cb084918e2214f5212f6c6 Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:41:24 -0700 Subject: [PATCH 064/952] feature -> setting --- lib/heroku/command/settings.rb | 82 +++++++++++++++++----------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index e210a9943..b13f30bf5 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -1,12 +1,12 @@ require "heroku/command/base" -# manage optional features +# manage optional settings # class Heroku::Command::Settings < Heroku::Command::Base # labs # - # list experimental features + # list experimental settings # #Example: # @@ -20,22 +20,22 @@ class Heroku::Command::Settings < Heroku::Command::Base def index validate_arguments! - user_features, app_features = api.get_features(app).body.sort_by do |feature| - feature["name"] - end.partition do |feature| - feature["kind"] == "user" + user_settings, app_settings = api.get_features(app).body.sort_by do |setting| + setting["name"] + end.partition do |setting| + setting["kind"] == "user" end - # general availability features are managed via `settings`, not `labs` - app_features.reject! { |f| f["state"] == "general" } + # general availability settings are managed via `settings`, not `labs` + app_settings.reject! { |f| f["state"] == "general" } display_app = app || "no app specified" styled_header "User Features (#{Heroku::Auth.user})" - display_features user_features + display_settings user_settings display styled_header "App Features (#{display_app})" - display_features app_features + display_settings app_settings end alias_command "labs:list", "labs" @@ -52,39 +52,39 @@ def index # Summary: Add user config vars to the environment during slug compilation # def info - unless feature_name = shift_argument + unless setting_name = shift_argument error("Usage: heroku labs:info SETTING\nMust specify SETTING for info.") end validate_arguments! - feature_data = api.get_feature(feature_name, app).body - styled_header(feature_data['name']) + setting_data = api.get_feature(setting_name, app).body + styled_header(setting_data['name']) styled_hash({ - 'Summary' => feature_data['summary'], - 'Docs' => feature_data['docs'] + 'Summary' => setting_data['summary'], + 'Docs' => setting_data['docs'] }) end # labs:disable SETTING # - # disables an experimental feature + # disables an experimental setting # #Example: # # $ heroku labs:disable ninja-power - # Disabling ninja-power feature for me@example.org... done + # Disabling ninja-power setting for me@example.org... done # def disable - feature_name = shift_argument - error "Usage: heroku labs:disable SETTING\nMust specify SETTING to disable." unless feature_name + setting_name = shift_argument + error "Usage: heroku labs:disable SETTING\nMust specify SETTING to disable." unless setting_name validate_arguments! - feature = api.get_features(app).body.detect { |f| f["name"] == feature_name } - message = "Disabling #{feature_name} " + setting = api.get_features(app).body.detect { |f| f["name"] == setting_name } + message = "Disabling #{setting_name} " - error "No such feature: #{feature_name}" unless feature + error "No such setting: #{setting_name}" unless setting - if feature["kind"] == "user" + if setting["kind"] == "user" message += "for #{Heroku::Auth.user}" else error "Must specify an app" unless app @@ -92,42 +92,42 @@ def disable end action message do - api.delete_feature feature_name, app + api.delete_setting setting_name, app end end # labs:enable SETTING # - # enables an experimental feature + # enables an experimental setting # #Example: # # $ heroku labs:enable ninja-power - # Enabling ninja-power feature for me@example.org... done + # Enabling ninja-power setting for me@example.org... done # def enable - feature_name = shift_argument - error "Usage: heroku labs:enable SETTING\nMust specify SETTING to enable." unless feature_name + setting_name = shift_argument + error "Usage: heroku labs:enable SETTING\nMust specify SETTING to enable." unless setting_name validate_arguments! - feature = api.get_features.body.detect { |f| f["name"] == feature_name } - message = "Enabling #{feature_name} " + setting = api.get_features.body.detect { |f| f["name"] == setting_name } + message = "Enabling #{setting_name} " - error "No such feature: #{feature_name}" unless feature + error "No such setting: #{setting_name}" unless setting - if feature["kind"] == "user" + if setting["kind"] == "user" message += "for #{Heroku::Auth.user}" else error "Must specify an app" unless app message += "for #{app}" end - feature_data = action(message) do - api.post_feature(feature_name, app).body + setting_data = action(message) do + api.post_setting(setting_name, app).body end - display "WARNING: This feature is experimental and may change or be removed without notice." - display "For more information see: #{feature_data["docs"]}" if feature_data["docs"] + display "WARNING: This setting is experimental and may change or be removed without notice." + display "For more information see: #{setting_data["docs"]}" if setting_data["docs"] end private @@ -139,11 +139,11 @@ def app nil end - def display_features(features) - longest_name = features.map { |f| f["name"].to_s.length }.sort.last - features.each do |feature| - toggle = feature["enabled"] ? "[+]" : "[ ]" - display "%s %-#{longest_name}s %s" % [ toggle, feature["name"], feature["summary"] ] + def display_settings(settings) + longest_name = settings.map { |f| f["name"].to_s.length }.sort.last + settings.each do |setting| + toggle = setting["enabled"] ? "[+]" : "[ ]" + display "%s %-#{longest_name}s %s" % [ toggle, setting["name"], setting["summary"] ] end end From f0a7b7a690d5a31a786aa9cbc45b87284ced79cb Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:41:46 -0700 Subject: [PATCH 065/952] Feature -> Setting --- lib/heroku/command/settings.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index b13f30bf5..46195f024 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -10,10 +10,10 @@ class Heroku::Command::Settings < Heroku::Command::Base # #Example: # - # === User Features (david@heroku.com) + # === User Settings (david@heroku.com) # [+] dashboard Use Heroku Dashboard by default # - # === App Features (glacial-retreat-5913) + # === App Settings (glacial-retreat-5913) # [ ] preboot Provide seamless web dyno deploys # [ ] user-env-compile Add user config vars to the environment during slug compilation # $ heroku labs -a example # @@ -31,10 +31,10 @@ def index display_app = app || "no app specified" - styled_header "User Features (#{Heroku::Auth.user})" + styled_header "User Settings (#{Heroku::Auth.user})" display_settings user_settings display - styled_header "App Features (#{display_app})" + styled_header "App Settings (#{display_app})" display_settings app_settings end From d7b9c44d2c02cf1d9d3bf179c0beee3c7469788b Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:42:50 -0700 Subject: [PATCH 066/952] labs -> settings --- lib/heroku/command/settings.rb | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index 46195f024..33906d465 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -4,7 +4,7 @@ # class Heroku::Command::Settings < Heroku::Command::Base - # labs + # settings # # list experimental settings # @@ -15,7 +15,7 @@ class Heroku::Command::Settings < Heroku::Command::Base # # === App Settings (glacial-retreat-5913) # [ ] preboot Provide seamless web dyno deploys - # [ ] user-env-compile Add user config vars to the environment during slug compilation # $ heroku labs -a example + # [ ] user-env-compile Add user config vars to the environment during slug compilation # $ heroku settings -a example # def index validate_arguments! @@ -26,7 +26,7 @@ def index setting["kind"] == "user" end - # general availability settings are managed via `settings`, not `labs` + # general availability settings are managed via `settings`, not `settings` app_settings.reject! { |f| f["state"] == "general" } display_app = app || "no app specified" @@ -38,22 +38,22 @@ def index display_settings app_settings end - alias_command "labs:list", "labs" + alias_command "settings:list", "settings" - # labs:info SETTING + # settings:info SETTING # # displays additional information about SETTING # #Example: # - # $ heroku labs:info user_env_compile + # $ heroku settings:info user_env_compile # === user_env_compile - # Docs: http://devcenter.heroku.com/articles/labs-user-env-compile + # Docs: http://devcenter.heroku.com/articles/settings-user-env-compile # Summary: Add user config vars to the environment during slug compilation # def info unless setting_name = shift_argument - error("Usage: heroku labs:info SETTING\nMust specify SETTING for info.") + error("Usage: heroku settings:info SETTING\nMust specify SETTING for info.") end validate_arguments! @@ -65,18 +65,18 @@ def info }) end - # labs:disable SETTING + # settings:disable SETTING # # disables an experimental setting # #Example: # - # $ heroku labs:disable ninja-power + # $ heroku settings:disable ninja-power # Disabling ninja-power setting for me@example.org... done # def disable setting_name = shift_argument - error "Usage: heroku labs:disable SETTING\nMust specify SETTING to disable." unless setting_name + error "Usage: heroku settings:disable SETTING\nMust specify SETTING to disable." unless setting_name validate_arguments! setting = api.get_features(app).body.detect { |f| f["name"] == setting_name } @@ -96,18 +96,18 @@ def disable end end - # labs:enable SETTING + # settings:enable SETTING # # enables an experimental setting # #Example: # - # $ heroku labs:enable ninja-power + # $ heroku settings:enable ninja-power # Enabling ninja-power setting for me@example.org... done # def enable setting_name = shift_argument - error "Usage: heroku labs:enable SETTING\nMust specify SETTING to enable." unless setting_name + error "Usage: heroku settings:enable SETTING\nMust specify SETTING to enable." unless setting_name validate_arguments! setting = api.get_features.body.detect { |f| f["name"] == setting_name } From 39f0f43d7b45dc3703333719edabd7db06c90efd Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:45:52 -0700 Subject: [PATCH 067/952] Copy. Remove references to experimental features --- lib/heroku/command/settings.rb | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index 33906d465..14a802f59 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -6,16 +6,12 @@ class Heroku::Command::Settings < Heroku::Command::Base # settings # - # list experimental settings + # list available settings # #Example: # - # === User Settings (david@heroku.com) - # [+] dashboard Use Heroku Dashboard by default - # # === App Settings (glacial-retreat-5913) # [ ] preboot Provide seamless web dyno deploys - # [ ] user-env-compile Add user config vars to the environment during slug compilation # $ heroku settings -a example # def index validate_arguments! @@ -46,10 +42,10 @@ def index # #Example: # - # $ heroku settings:info user_env_compile - # === user_env_compile - # Docs: http://devcenter.heroku.com/articles/settings-user-env-compile - # Summary: Add user config vars to the environment during slug compilation + # $ heroku settings:info preboot + # === preboot + # Docs: https://devcenter.heroku.com/articles/preboot + # Summary: Provide seamless web dyno deploys # def info unless setting_name = shift_argument @@ -67,12 +63,12 @@ def info # settings:disable SETTING # - # disables an experimental setting + # disables a setting # #Example: # - # $ heroku settings:disable ninja-power - # Disabling ninja-power setting for me@example.org... done + # $ heroku settings:disable preboot + # Disabling preboot setting for me@example.org... done # def disable setting_name = shift_argument @@ -98,12 +94,12 @@ def disable # settings:enable SETTING # - # enables an experimental setting + # enables an setting # #Example: # - # $ heroku settings:enable ninja-power - # Enabling ninja-power setting for me@example.org... done + # $ heroku settings:enable preboot + # Enabling preboot setting for me@example.org... done # def enable setting_name = shift_argument From 4e9841ebfddda36e5f313a500aea94216cfddebf Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:47:23 -0700 Subject: [PATCH 068/952] only display "general" app settings --- lib/heroku/command/settings.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index 14a802f59..d736880eb 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -16,20 +16,16 @@ class Heroku::Command::Settings < Heroku::Command::Base def index validate_arguments! - user_settings, app_settings = api.get_features(app).body.sort_by do |setting| - setting["name"] - end.partition do |setting| - setting["kind"] == "user" + app_settings = api.get_features(app).body.select do |feature| + feature["state"] == "general" end - # general availability settings are managed via `settings`, not `settings` - app_settings.reject! { |f| f["state"] == "general" } + app_settings.sort_by! do |feature| + feature["name"] + end display_app = app || "no app specified" - styled_header "User Settings (#{Heroku::Auth.user})" - display_settings user_settings - display styled_header "App Settings (#{display_app})" display_settings app_settings end From c2c6e2bd87616b54b19a0509ff95f53a90c217b0 Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:52:37 -0700 Subject: [PATCH 069/952] still need to use api.delete_feature or api.post_feature --- lib/heroku/command/settings.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index d736880eb..253ae3c44 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -84,7 +84,7 @@ def disable end action message do - api.delete_setting setting_name, app + api.delete_feature setting_name, app end end @@ -115,7 +115,7 @@ def enable end setting_data = action(message) do - api.post_setting(setting_name, app).body + api.post_feature(setting_name, app).body end display "WARNING: This setting is experimental and may change or be removed without notice." From c84b47f288206edd3ef20ce9079d6be7e238c36f Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 11:54:47 -0700 Subject: [PATCH 070/952] remove WARNING --- lib/heroku/command/settings.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index 253ae3c44..feddb421b 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -118,7 +118,6 @@ def enable api.post_feature(setting_name, app).body end - display "WARNING: This setting is experimental and may change or be removed without notice." display "For more information see: #{setting_data["docs"]}" if setting_data["docs"] end From ce4648d21843acaeb2fc4aad33480d733a5e11fd Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 14:28:34 -0700 Subject: [PATCH 071/952] setting -> feature --- lib/heroku/command/settings.rb | 96 +++++++++++++++++----------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/settings.rb index feddb421b..7abc48105 100644 --- a/lib/heroku/command/settings.rb +++ b/lib/heroku/command/settings.rb @@ -1,82 +1,82 @@ require "heroku/command/base" -# manage optional settings +# manage optional features # -class Heroku::Command::Settings < Heroku::Command::Base +class Heroku::Command::Features < Heroku::Command::Base - # settings + # features # - # list available settings + # list available features # #Example: # - # === App Settings (glacial-retreat-5913) + # === App Features (glacial-retreat-5913) # [ ] preboot Provide seamless web dyno deploys # def index validate_arguments! - app_settings = api.get_features(app).body.select do |feature| + app_features = api.get_features(app).body.select do |feature| feature["state"] == "general" end - app_settings.sort_by! do |feature| + app_features.sort_by! do |feature| feature["name"] end display_app = app || "no app specified" - styled_header "App Settings (#{display_app})" - display_settings app_settings + styled_header "App Features (#{display_app})" + display_features app_features end - alias_command "settings:list", "settings" + alias_command "features:list", "features" - # settings:info SETTING + # features:info FEATURE # - # displays additional information about SETTING + # displays additional information about FEATURE # #Example: # - # $ heroku settings:info preboot + # $ heroku features:info preboot # === preboot # Docs: https://devcenter.heroku.com/articles/preboot # Summary: Provide seamless web dyno deploys # def info - unless setting_name = shift_argument - error("Usage: heroku settings:info SETTING\nMust specify SETTING for info.") + unless feature_name = shift_argument + error("Usage: heroku feature:info FEATURE\nMust specify FEATURE for info.") end validate_arguments! - setting_data = api.get_feature(setting_name, app).body - styled_header(setting_data['name']) + feature_data = api.get_feature(feature_name, app).body + styled_header(feature_data['name']) styled_hash({ - 'Summary' => setting_data['summary'], - 'Docs' => setting_data['docs'] + 'Summary' => feature_data['summary'], + 'Docs' => feature_data['docs'] }) end - # settings:disable SETTING + # features:disable FEATURE # - # disables a setting + # disables a feature # #Example: # - # $ heroku settings:disable preboot - # Disabling preboot setting for me@example.org... done + # $ heroku features:disable preboot + # Disabling preboot feature for me@example.org... done # def disable - setting_name = shift_argument - error "Usage: heroku settings:disable SETTING\nMust specify SETTING to disable." unless setting_name + feature_name = shift_argument + error "Usage: heroku feature:disable FEATURE\nMust specify FEATURE to disable." unless feature_name validate_arguments! - setting = api.get_features(app).body.detect { |f| f["name"] == setting_name } - message = "Disabling #{setting_name} " + feature = api.get_features(app).body.detect { |f| f["name"] == feature_name } + message = "Disabling #{feature_name} " - error "No such setting: #{setting_name}" unless setting + error "No such feature: #{feature_name}" unless feature - if setting["kind"] == "user" + if feature["kind"] == "user" message += "for #{Heroku::Auth.user}" else error "Must specify an app" unless app @@ -84,41 +84,41 @@ def disable end action message do - api.delete_feature setting_name, app + api.delete_feature feature_name, app end end - # settings:enable SETTING + # feature:enable FEATURE # - # enables an setting + # enables an feature # #Example: # - # $ heroku settings:enable preboot - # Enabling preboot setting for me@example.org... done + # $ heroku features:enable preboot + # Enabling preboot feature for me@example.org... done # def enable - setting_name = shift_argument - error "Usage: heroku settings:enable SETTING\nMust specify SETTING to enable." unless setting_name + feature_name = shift_argument + error "Usage: heroku features:enable FEATURE\nMust specify FEATURE to enable." unless feature_name validate_arguments! - setting = api.get_features.body.detect { |f| f["name"] == setting_name } - message = "Enabling #{setting_name} " + feature = api.get_features.body.detect { |f| f["name"] == feature_name } + message = "Enabling #{feature_name} " - error "No such setting: #{setting_name}" unless setting + error "No such feature: #{feature_name}" unless feature - if setting["kind"] == "user" + if feature["kind"] == "user" message += "for #{Heroku::Auth.user}" else error "Must specify an app" unless app message += "for #{app}" end - setting_data = action(message) do - api.post_feature(setting_name, app).body + feature_data = action(message) do + api.post_feature(feature_name, app).body end - display "For more information see: #{setting_data["docs"]}" if setting_data["docs"] + display "For more information see: #{feature_data["docs"]}" if feature_data["docs"] end private @@ -130,11 +130,11 @@ def app nil end - def display_settings(settings) - longest_name = settings.map { |f| f["name"].to_s.length }.sort.last - settings.each do |setting| - toggle = setting["enabled"] ? "[+]" : "[ ]" - display "%s %-#{longest_name}s %s" % [ toggle, setting["name"], setting["summary"] ] + def display_features(features) + longest_name = features.map { |f| f["name"].to_s.length }.sort.last + features.each do |feature| + toggle = feature["enabled"] ? "[+]" : "[ ]" + display "%s %-#{longest_name}s %s" % [ toggle, feature["name"], feature["summary"] ] end end From 34a408852627b5d526e9edaba65ff9cbb88a6ec8 Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 14:29:01 -0700 Subject: [PATCH 072/952] settings.rb -> features.rb --- lib/heroku/command/{settings.rb => features.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/heroku/command/{settings.rb => features.rb} (100%) diff --git a/lib/heroku/command/settings.rb b/lib/heroku/command/features.rb similarity index 100% rename from lib/heroku/command/settings.rb rename to lib/heroku/command/features.rb From 5c4492ed47ef579889a0a14a1a1c3aea413b9040 Mon Sep 17 00:00:00 2001 From: Noah Zoschke Date: Wed, 24 Sep 2014 14:30:00 -0700 Subject: [PATCH 073/952] only list app features --- lib/heroku/command/features.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/features.rb b/lib/heroku/command/features.rb index 7abc48105..3564a737f 100644 --- a/lib/heroku/command/features.rb +++ b/lib/heroku/command/features.rb @@ -17,7 +17,7 @@ def index validate_arguments! app_features = api.get_features(app).body.select do |feature| - feature["state"] == "general" + feature["kind"] == "app" && feature["state"] == "general" end app_features.sort_by! do |feature| From 264d1da9366fc8c9ad0bc18fc627bf243cce1dd5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 26 Sep 2014 15:01:39 -0400 Subject: [PATCH 074/952] v3.11.2 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a99e8008d..ab19922e7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.11.1) + heroku (3.11.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 11eca788e..5d726853e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.11.1" + VERSION = "3.11.2" end From 1bc71e3ec483abf96f8931b1ed88439f3df186a3 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Mon, 29 Sep 2014 14:44:25 -0700 Subject: [PATCH 075/952] Replace code. with git. in .netrc setup --- lib/heroku/auth.rb | 4 ++-- spec/heroku/auth_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 5387a5d22..4b54fd9c4 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -101,7 +101,7 @@ def delete_credentials end if netrc netrc.delete("api.#{host}") - netrc.delete("code.#{host}") + netrc.delete("git.#{host}") netrc.save end @api, @client, @credentials = nil, nil @@ -172,7 +172,7 @@ def write_credentials FileUtils.chmod(0600, netrc_path) end netrc["api.#{host}"] = self.credentials - netrc["code.#{host}"] = self.credentials + netrc["git.#{host}"] = self.credentials netrc.save end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index 5e50b1614..97a80671b 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -27,7 +27,7 @@ module Heroku File.open(@cli.netrc_path, "w") do |file| file.puts("machine api.heroku.com\n login user\n password pass\n") - file.puts("machine code.heroku.com\n login user\n password pass\n") + file.puts("machine git.heroku.com\n login user\n password pass\n") end end @@ -178,7 +178,7 @@ module Heroku @cli.netrc["api.#{@cli.host}"] = ["user", api_key] expect(@cli.get_credentials).to eq(["user", api_key[0,40]]) - %w{api code}.each do |section| + %w{api git}.each do |section| expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"]).to eq(["user", api_key[0,40]]) end end From 7a57c76abd8af436c85a21e1e53270aedba3ba82 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Mon, 29 Sep 2014 15:11:41 -0700 Subject: [PATCH 076/952] DRY up auth subdomains --- lib/heroku/auth.rb | 14 ++++++++++---- spec/heroku/auth_spec.rb | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 4b54fd9c4..5c771b2df 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -61,6 +61,10 @@ def host ENV['HEROKU_HOST'] || default_host end + def subdomains + %w(api git) + end + def reauthorize @credentials = ask_for_and_save_credentials end @@ -100,8 +104,9 @@ def delete_credentials FileUtils.rm_f(legacy_credentials_path) end if netrc - netrc.delete("api.#{host}") - netrc.delete("git.#{host}") + subdomains.each do |sub| + netrc.delete("#{sub}.#{host}") + end netrc.save end @api, @client, @credentials = nil, nil @@ -171,8 +176,9 @@ def write_credentials unless running_on_windows? FileUtils.chmod(0600, netrc_path) end - netrc["api.#{host}"] = self.credentials - netrc["git.#{host}"] = self.credentials + subdomains.each do |sub| + netrc["#{sub}.#{host}"] = self.credentials + end netrc.save end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index 97a80671b..b2048b51d 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -178,7 +178,7 @@ module Heroku @cli.netrc["api.#{@cli.host}"] = ["user", api_key] expect(@cli.get_credentials).to eq(["user", api_key[0,40]]) - %w{api git}.each do |section| + Auth.subdomains.each do |section| expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"]).to eq(["user", api_key[0,40]]) end end From 0a93fac5103a93f01a50b19f5e1782cdc6e1afc4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 29 Sep 2014 18:26:15 -0700 Subject: [PATCH 077/952] always preauth --- lib/heroku.rb | 8 ++++++++ lib/heroku/auth.rb | 8 +++++++- lib/heroku/command/base.rb | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/heroku.rb b/lib/heroku.rb index 6141d06e5..d35bcca34 100644 --- a/lib/heroku.rb +++ b/lib/heroku.rb @@ -3,6 +3,7 @@ require "heroku/version" module Heroku + @@app_name = nil USER_AGENT = "heroku-gem/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" @@ -14,4 +15,11 @@ def self.user_agent=(agent) @@user_agent = agent end + def self.app_name + @@app_name + end + + def self.app_name=(app_name) + @@app_name = app_name + end end diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 5c771b2df..b57dcba3d 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -211,7 +211,13 @@ def ask_for_second_factor @two_factor_code = ask @two_factor_code = nil if @two_factor_code == "" @api = nil # reset it - @two_factor_code + preauth + end + + def preauth + if Heroku.app_name + api.request(:method => :put, :path => "/apps/#{Heroku.app_name}/pre-authorizations") + end end def ask_for_password_on_windows diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 11374ab19..903983569 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -20,7 +20,7 @@ def initialize(args=[], options={}) end def app - @app ||= if options[:confirm].is_a?(String) + @app ||= Heroku.app_name = if options[:confirm].is_a?(String) if options[:app] && (options[:app] != options[:confirm]) error("Mismatch between --app and --confirm") end From f8cbd1a4289bf134d4d5d96241b62c70df190730 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Oct 2014 16:58:59 -0700 Subject: [PATCH 078/952] v3.11.3 --- CHANGELOG | 11 ++++++++--- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4fa2b1878..e69f71669 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,12 @@ -3.11.1 2014-09-12 +3.11.3 2014-10-02 ================= -Attempt to push ~/.ssh/id_rsa SSH key instead of any one key -Always prompt for uploading SSH keys +Replace code. with git. in .netrc +Always use preauth for 2fa + +3.11.2 2014-09-26 +================= +Use server-side connection for pg:killall +Send orgs requests to api.heroku.com 3.10.6 2014-09-04 ================= diff --git a/Gemfile.lock b/Gemfile.lock index ab19922e7..b5e0697e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.11.2) + heroku (3.11.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 5d726853e..63470a60e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.11.2" + VERSION = "3.11.3" end From 28cd5764c5ea0297ba653885628a3d10d794048d Mon Sep 17 00:00:00 2001 From: adi1133 Date: Sat, 4 Oct 2014 20:32:52 +0300 Subject: [PATCH 079/952] Fix debian ruby module dependency Remove libopenssl and libreadline dependency for ruby>1.9 --- dist/resources/deb/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/resources/deb/control b/dist/resources/deb/control index 5672073b7..c7f9a96df 100644 --- a/dist/resources/deb/control +++ b/dist/resources/deb/control @@ -3,6 +3,6 @@ Version: <%= version %> Section: main Priority: standard Architecture: all -Depends: ruby2.1|ruby2.0|ruby1.9.1, libopenssl-ruby2.1|libopenssl-ruby2.0|libopenssl-ruby1.9.1, libreadline-ruby2.1|libreadline-ruby2.0|libreadline-ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 +Depends: ruby2.1|ruby2.0|libopenssl-ruby1.9.1, ruby2.1|ruby2.0|libreadline-ruby1.9.1, ruby2.1|ruby2.0|ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 Maintainer: Heroku Description: Client library and CLI to deploy apps on Heroku. From 9963516839a7b192e4a22c65c28b33f62ad8b65b Mon Sep 17 00:00:00 2001 From: Jonathan Dance Date: Sun, 5 Oct 2014 17:13:41 -0700 Subject: [PATCH 080/952] unique warnings before output --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index e9e6ede75..7c125d2c4 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -99,7 +99,7 @@ def self.warnings def self.display_warnings unless warnings.empty? - $stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n")) + $stderr.puts(warnings.uniq.map {|warning| " ! #{warning}"}.join("\n")) end end From 2e9910a0236a11fad17045ce76d6818c03b83eea Mon Sep 17 00:00:00 2001 From: Brandur Date: Mon, 6 Oct 2014 08:05:49 -0700 Subject: [PATCH 081/952] Bump Excon to 0.40.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a99e8008d..f498b9819 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,7 +27,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.39.5) + excon (0.40.0) fakefs (0.5.2) heroku-api (0.3.19) excon (~> 0.38) From 4421c5942c14ae3e48ffe31c600af8198f4a3bf7 Mon Sep 17 00:00:00 2001 From: Jesper Joergensen Date: Tue, 30 Sep 2014 17:26:37 -0700 Subject: [PATCH 082/952] add --http-git option to create, git:clone and git:remote --- lib/heroku/command/apps.rb | 11 +++++++++-- lib/heroku/command/git.rb | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 6e5ce8deb..b1e5842eb 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -196,6 +196,7 @@ def info # --region REGION # specify region for this app to run in # -l, --locked # lock the app # -t, --tier TIER # HIDDEN: the tier for this app + # --http-git # Use HTTP git protocol # #Examples: # @@ -236,6 +237,12 @@ def create api.post_app(params).body end + git_url = if options[:http_git] + "https://git.heroku.com/#{name}.git" + else + info["git_url"] + end + begin action("Creating #{info['name']}", :org => !!org) do if info['create_status'] == 'creating' @@ -266,13 +273,13 @@ def create display("BUILDPACK_URL=#{buildpack}") end - hputs([ info["web_url"], info["git_url"] ].join(" | ")) + hputs([ info["web_url"], git_url ].join(" | ")) rescue Timeout::Error hputs("Timed Out! Run `heroku status` to check for known platform issues.") end unless options[:no_remote].is_a? FalseClass - create_git_remote(options[:remote] || "heroku", info["git_url"]) + create_git_remote(options[:remote] || "heroku", git_url) end end diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index b54e80076..03da8493e 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -9,6 +9,8 @@ class Heroku::Command::Git < Heroku::Command::Base # clones a heroku app to your local machine at DIRECTORY (defaults to app name) # # -r, --remote REMOTE # the git remote to create, default "heroku" + # --http-git # Use HTTP git protocol + # # #Examples: # @@ -25,7 +27,11 @@ def clone directory = shift_argument validate_arguments! - git_url = api.get_app(name).body["git_url"] + git_url = if options[:http_git] + "https://git.heroku.com/#{name}.git" + else + api.get_app(name).body["git_url"] + end puts "Cloning from app '#{name}'..." system "git clone -o #{remote} #{git_url} #{directory}".strip @@ -40,6 +46,7 @@ def clone # if OPTIONS are specified they will be passed to git remote add # # -r, --remote REMOTE # the git remote to create, default "heroku" + # --http-git # Use HTTP git protocol # #Examples: # @@ -57,7 +64,12 @@ def remote error("Git remote #{remote} already exists") else app_data = api.get_app(app).body - create_git_remote(remote, app_data['git_url']) + git_url = if options[:http_git] + "https://git.heroku.com/#{app_data['name']}" + else + app_data['git_url'] + end + create_git_remote(remote, git_url) end end From db80cb88a08663cf58590639ad14e03fb68f2a5b Mon Sep 17 00:00:00 2001 From: Jesper Joergensen Date: Thu, 2 Oct 2014 11:54:58 -0700 Subject: [PATCH 083/952] forgot .git in the end of url --- lib/heroku/command/git.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 03da8493e..06a3ab2a0 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -65,7 +65,7 @@ def remote else app_data = api.get_app(app).body git_url = if options[:http_git] - "https://git.heroku.com/#{app_data['name']}" + "https://git.heroku.com/#{app_data['name']}.git" else app_data['git_url'] end From 2ea4da5996069a328307bca32e02c3f554bdc657 Mon Sep 17 00:00:00 2001 From: Jesper Joergensen Date: Thu, 2 Oct 2014 22:05:04 -0700 Subject: [PATCH 084/952] Detect app name from git remote correctly when using http git --- lib/heroku/command/base.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 903983569..bc7bccf5f 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -268,7 +268,8 @@ def git_remotes(base_dir=Dir.pwd) return unless File.exists?(".git") git("remote -v").split("\n").each do |remote| name, url, method = remote.split(/\s/) - if url =~ /^git@#{Heroku::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/ + if url =~ /^git@#{Heroku::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/ || + url =~ /^https:\/\/git.#{Heroku::Auth.git_host}\/([\w\d-]+)\.git$/ remotes[name] = $1 end end From 90bfb6dc54a67f4dc866c8abf9c3e41b57314fec Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 6 Oct 2014 16:18:44 -0700 Subject: [PATCH 085/952] v3.12.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e69f71669..42a0dc140 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.12.0 2014-10-06 +================= +Excon 0.40.0 +Unique output warnings +More git options + 3.11.3 2014-10-02 ================= Replace code. with git. in .netrc diff --git a/Gemfile.lock b/Gemfile.lock index e45f0a1e5..6a4532b11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.11.3) + heroku (3.12.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) @@ -27,7 +27,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.40.0) + excon (0.39.5) fakefs (0.5.2) heroku-api (0.3.19) excon (~> 0.38) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 63470a60e..c88f4dc34 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.11.3" + VERSION = "3.12.0" end From 571b1c5bab511d63f5b9c689576b6f33fee874dd Mon Sep 17 00:00:00 2001 From: Daniel Farina Date: Mon, 6 Oct 2014 19:46:33 -0700 Subject: [PATCH 086/952] Pass symbolic option to config_var endpoint When passed, symbolic representations of attached resources are rendered instead of their actual values. One also requires a feature flag to see this. --- lib/heroku/command/config.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index e3d160372..cce5185b8 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -23,7 +23,17 @@ class Heroku::Command::Config < Heroku::Command::Base def index validate_arguments! - vars = api.get_config_vars(app).body + vars = if options[:shell] + api.get_config_vars(app).body + else + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}/config_vars", + :query => { "symbolic" => true } + ).body + end + if vars.empty? display("#{app} has no config vars.") else From 1f488fc29d4139dd0e7332b08d952d8d36577e00 Mon Sep 17 00:00:00 2001 From: Brandur Date: Mon, 6 Oct 2014 19:53:07 -0700 Subject: [PATCH 087/952] Re-bump Excon to 0.40.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6a4532b11..3ae635772 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -27,7 +27,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.39.5) + excon (0.40.0) fakefs (0.5.2) heroku-api (0.3.19) excon (~> 0.38) From 1704d596cc8b4a24439e1331430973270f684894 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 7 Oct 2014 14:50:13 -0700 Subject: [PATCH 088/952] hide http git for now --- lib/heroku/command/apps.rb | 2 +- lib/heroku/command/git.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index b1e5842eb..e1ced05c4 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -196,7 +196,7 @@ def info # --region REGION # specify region for this app to run in # -l, --locked # lock the app # -t, --tier TIER # HIDDEN: the tier for this app - # --http-git # Use HTTP git protocol + # --http-git # HIDDEN: Use HTTP git protocol # #Examples: # diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 06a3ab2a0..d8610e2fa 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -9,7 +9,7 @@ class Heroku::Command::Git < Heroku::Command::Base # clones a heroku app to your local machine at DIRECTORY (defaults to app name) # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --http-git # Use HTTP git protocol + # --http-git # HIDDEN: Use HTTP git protocol # # #Examples: @@ -46,7 +46,7 @@ def clone # if OPTIONS are specified they will be passed to git remote add # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --http-git # Use HTTP git protocol + # --http-git # HIDDEN: Use HTTP git protocol # #Examples: # From 94da2748c7f004f2052c7615c167754afe8143b3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 7 Oct 2014 15:30:12 -0700 Subject: [PATCH 089/952] use variables to find http git host --- lib/heroku/auth.rb | 4 ++++ lib/heroku/command/apps.rb | 2 +- lib/heroku/command/base.rb | 4 ++-- lib/heroku/command/git.rb | 6 ++---- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index b57dcba3d..7f32c48dc 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -53,6 +53,10 @@ def default_host "heroku.com" end + def http_git_host + ENV['HEROKU_HTTP_GIT_HOST'] || "git.#{host}" + end + def git_host ENV['HEROKU_GIT_HOST'] || host end diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index e1ced05c4..cbea1de1f 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -238,7 +238,7 @@ def create end git_url = if options[:http_git] - "https://git.heroku.com/#{name}.git" + "https://#{Heroku::Auth.http_git_host}/#{info['name']}.git" else info["git_url"] end diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index bc7bccf5f..a7a797b77 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -267,9 +267,9 @@ def git_remotes(base_dir=Dir.pwd) return unless File.exists?(".git") git("remote -v").split("\n").each do |remote| - name, url, method = remote.split(/\s/) + name, url, _ = remote.split(/\s/) if url =~ /^git@#{Heroku::Auth.git_host}(?:[\.\w]*):([\w\d-]+)\.git$/ || - url =~ /^https:\/\/git.#{Heroku::Auth.git_host}\/([\w\d-]+)\.git$/ + url =~ /^https:\/\/#{Heroku::Auth.http_git_host}\/([\w\d-]+)\.git$/ remotes[name] = $1 end end diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index d8610e2fa..b14973bb0 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -28,7 +28,7 @@ def clone validate_arguments! git_url = if options[:http_git] - "https://git.heroku.com/#{name}.git" + "https://#{Heroku::Auth.http_git_host}/#{name}.git" else api.get_app(name).body["git_url"] end @@ -57,7 +57,6 @@ def clone # ! Git remote heroku already exists # def remote - git_options = args.join(" ") remote = options[:remote] || 'heroku' if git('remote').split("\n").include?(remote) @@ -65,12 +64,11 @@ def remote else app_data = api.get_app(app).body git_url = if options[:http_git] - "https://git.heroku.com/#{app_data['name']}.git" + "https://#{Heroku::Auth.http_git_host}/#{app_data['name']}.git" else app_data['git_url'] end create_git_remote(remote, git_url) end end - end From 3adf173ddff9116367fc679334d8b673c825869f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 7 Oct 2014 15:49:49 -0700 Subject: [PATCH 090/952] v3.12.1 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 42a0dc140..6f12cc5a0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.12.1 2014-10-07 +================= +Fixed Excon 0.40.0 in Gemfile +Fixed git finders to work with env vars +Symbolic config vars + 3.12.0 2014-10-06 ================= Excon 0.40.0 diff --git a/Gemfile.lock b/Gemfile.lock index 3ae635772..9b2b5e699 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.12.0) + heroku (3.12.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) netrc (~> 0.7.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index c88f4dc34..29acbc766 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.12.0" + VERSION = "3.12.1" end From c7b29096a063bcec760dd5356c0b8f8830097129 Mon Sep 17 00:00:00 2001 From: Daniel Farina Date: Wed, 8 Oct 2014 18:01:12 -0700 Subject: [PATCH 091/952] Deprecate heroku-symbol plugin The functionality was merged previously in 571b1c5bab511d63f5b9c689576b6f33fee874dd. --- lib/heroku/plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index ae6291bd9..11778d770 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -24,6 +24,7 @@ class ErrorUpdatingSymlinkPlugin < StandardError; end heroku-status heroku-stop heroku-suggest + heroku-symbol heroku-two-factor pgbackups-automate pgcmd From 75eb62fa155b35b0edc19dc39e5b303369e8cec2 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Thu, 9 Oct 2014 15:38:04 -0700 Subject: [PATCH 092/952] Allow git:remote to update remotes with set-url --- lib/heroku/command/git.rb | 18 +++++++++++------- lib/heroku/helpers.rb | 7 +++++++ spec/heroku/command/git_spec.rb | 22 +++++++++++++++------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index b14973bb0..36b4d392e 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -46,6 +46,7 @@ def clone # if OPTIONS are specified they will be passed to git remote add # # -r, --remote REMOTE # the git remote to create, default "heroku" + # --update # update the remote if it already exists # --http-git # HIDDEN: Use HTTP git protocol # #Examples: @@ -58,16 +59,19 @@ def clone # def remote remote = options[:remote] || 'heroku' + app_data = api.get_app(app).body + git_url = if options[:http_git] + "https://#{Heroku::Auth.http_git_host}/#{app_data['name']}.git" + else + app_data['git_url'] + end if git('remote').split("\n").include?(remote) - error("Git remote #{remote} already exists") - else - app_data = api.get_app(app).body - git_url = if options[:http_git] - "https://#{Heroku::Auth.http_git_host}/#{app_data['name']}.git" - else - app_data['git_url'] + q = "Git remote #{remote} already exists. Would you like to update it? (y/n)" + if options[:update] || confirm(q) + update_git_remote(remote, git_url) end + else create_git_remote(remote, git_url) end end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 2e618d8f9..3513ef66a 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -149,6 +149,13 @@ def create_git_remote(remote, url) display "Git remote #{remote} added" end + def update_git_remote(remote, url) + return unless git('remote').split("\n").include?(remote) + return unless File.exists?(".git") + git "remote set-url #{remote} #{url}" + display "Git remote #{remote} updated" + end + def longest(items) items.map { |i| i.to_s.length }.sort.last end diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb index b57adc4e0..876c30306 100644 --- a/spec/heroku/command/git_spec.rb +++ b/spec/heroku/command/git_spec.rb @@ -127,18 +127,26 @@ module Heroku::Command STDOUT end - it "skips remote when it already exists" do + it "updates remote when it already exists if update flag is set" do any_instance_of(Heroku::Command::Git) do |git| stub(git).git('remote').returns("heroku") + stub(git).git('remote set-url heroku git@heroku.com:example.git') end - stderr, stdout = execute("git:remote") - expect(stderr).to eq <<-STDERR - ! Git remote heroku already exists -STDERR - expect(stdout).to eq("") + stderr, stdout = execute("git:remote --update") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Git remote heroku updated + STDOUT end + it "prompts to updates remote when it already exists if update flag is not set" do + any_instance_of(Heroku::Command::Git) do |git| + stub(git).git('remote').returns("heroku") + end + stderr, stdout = execute("git:remote") + expect(stderr).to eq("") + expect(stdout).to eq "Git remote heroku already exists. Would you like to update it? (y/n) " + end end - end end From 8fbafb6dc64ab8e4aedb09dad5dd723d96f75299 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Thu, 9 Oct 2014 16:09:51 -0700 Subject: [PATCH 093/952] Clean up create/update_git_remote - Extract out has_git_remote? - Do not check for .git. Git does this for us and will work from sub directories. Instead, check for $?.success? - Only show success message on success --- lib/heroku/helpers.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 3513ef66a..af738034d 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -142,18 +142,20 @@ def quantify(string, num) "%d %s" % [ num, num.to_i == 1 ? string : "#{string}s" ] end + def has_git_remote?(remote) + git('remote').split("\n").include?(remote) && $?.success? + end + def create_git_remote(remote, url) - return if git('remote').split("\n").include?(remote) - return unless File.exists?(".git") + return if has_git_remote? remote git "remote add #{remote} #{url}" - display "Git remote #{remote} added" + display "Git remote #{remote} added" if $?.success? end def update_git_remote(remote, url) - return unless git('remote').split("\n").include?(remote) - return unless File.exists?(".git") + return unless has_git_remote? remote git "remote set-url #{remote} #{url}" - display "Git remote #{remote} updated" + display "Git remote #{remote} updated" if $?.success? end def longest(items) From cce296aadcb44198285cf4412f72ee31b832bda1 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Thu, 9 Oct 2014 16:12:15 -0700 Subject: [PATCH 094/952] Update git:remote command doc --- lib/heroku/command/git.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 36b4d392e..75e742c84 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -55,7 +55,7 @@ def clone # Git remote heroku added # # $ heroku git:remote -a example - # ! Git remote heroku already exists + # Git remote staging already exists. Would you like to update it? (y/n) # def remote remote = options[:remote] || 'heroku' From 860a17188e06893b6b5d1080052040d0a264db36 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Fri, 10 Oct 2014 12:10:06 -0700 Subject: [PATCH 095/952] Correct wording of remote update doc --- lib/heroku/command/git.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 75e742c84..4e0d9ce6c 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -55,7 +55,7 @@ def clone # Git remote heroku added # # $ heroku git:remote -a example - # Git remote staging already exists. Would you like to update it? (y/n) + # Git remote heroku already exists. Would you like to update it? (y/n) # def remote remote = options[:remote] || 'heroku' From 6dadf8a967ebf3736c1aed1886ebad03be6ee60a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 14 Oct 2014 19:02:59 -0700 Subject: [PATCH 096/952] switch to multi_json --- Gemfile.lock | 1 + Rakefile | 1 - heroku.gemspec | 1 + lib/heroku/client/organizations.rb | 2 +- lib/heroku/command/certs.rb | 2 +- lib/heroku/helpers.rb | 10 +- lib/vendor/heroku/okjson.rb | 598 ----------------------------- spec/heroku/command/addons_spec.rb | 18 +- spec/heroku/command/apps_spec.rb | 6 +- spec/heroku/command/orgs_spec.rb | 8 +- spec/heroku/command/pg_spec.rb | 2 +- spec/heroku/command/status_spec.rb | 2 +- 12 files changed, 26 insertions(+), 625 deletions(-) delete mode 100644 lib/vendor/heroku/okjson.rb diff --git a/Gemfile.lock b/Gemfile.lock index 9b2b5e699..0b39216ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,6 +4,7 @@ PATH heroku (3.12.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) + multi_json (~> 1.10.1) netrc (~> 0.7.7) rest-client (= 1.6.7) rubyzip (= 0.9.9) diff --git a/Rakefile b/Rakefile index df648ff65..c759e163a 100644 --- a/Rakefile +++ b/Rakefile @@ -136,7 +136,6 @@ Dir[File.expand_path("../dist/**/*.rake", __FILE__)].each do |rake| end def poll_ci - require("vendor/heroku/okjson") require("net/http") data = Heroku::OkJson.decode(Net::HTTP.get("travis-ci.org", "/heroku/heroku.json")) case data["last_build_status"] diff --git a/heroku.gemspec b/heroku.gemspec index 668e83b04..37a6109a6 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -25,4 +25,5 @@ Gem::Specification.new do |gem| gem.add_dependency "netrc", "~> 0.7.7" gem.add_dependency "rest-client", "= 1.6.7" gem.add_dependency "rubyzip", "= 0.9.9" + gem.add_dependency "multi_json", "~> 1.10.1" end diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index dea83814d..8415bebcc 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -56,7 +56,7 @@ def request params if response.body && !response.body.empty? decompress_response!(response) begin - response.body = Heroku::OkJson.decode(response.body) + response.body = MultiJson.load(response.body) rescue # leave non-JSON body as is end diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 573013832..df9db62b6 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -212,7 +212,7 @@ def post_to_ssl_doctor(path, action_text = nil) def read_crt_and_key_through_ssl_doctor(action_text = nil) crt_and_key = post_to_ssl_doctor("resolve-chain-and-key", action_text) - Heroku::OkJson.decode(crt_and_key).values_at("pem", "key") + MultiJson.load(crt_and_key).values_at("pem", "key") end def read_crt_through_ssl_doctor(action_text = nil) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 2e618d8f9..46522067d 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -1,5 +1,3 @@ -require "vendor/heroku/okjson" - module Heroku module Helpers @@ -178,14 +176,14 @@ def display_row(row, lengths) end def json_encode(object) - Heroku::OkJson.encode(object) - rescue Heroku::OkJson::Error + MultiJson.dump(object) + rescue MultiJson::ParseError nil end def json_decode(json) - Heroku::OkJson.decode(json) - rescue Heroku::OkJson::Error + MultiJson.load(json) + rescue MultiJson::ParseError nil end diff --git a/lib/vendor/heroku/okjson.rb b/lib/vendor/heroku/okjson.rb deleted file mode 100644 index 72aa6c81b..000000000 --- a/lib/vendor/heroku/okjson.rb +++ /dev/null @@ -1,598 +0,0 @@ -# encoding: UTF-8 -# -# Copyright 2011, 2012 Keith Rarick -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -# See https://github.com/kr/okjson for updates. - -require 'stringio' - -# Some parts adapted from -# http://golang.org/src/pkg/json/decode.go and -# http://golang.org/src/pkg/utf8/utf8.go -module Heroku - module OkJson - extend self - - - # Decodes a json document in string s and - # returns the corresponding ruby value. - # String s must be valid UTF-8. If you have - # a string in some other encoding, convert - # it first. - # - # String values in the resulting structure - # will be UTF-8. - def decode(s) - ts = lex(s) - v, ts = textparse(ts) - if ts.length > 0 - raise Error, 'trailing garbage' - end - v - end - - - # Parses a "json text" in the sense of RFC 4627. - # Returns the parsed value and any trailing tokens. - # Note: this is almost the same as valparse, - # except that it does not accept atomic values. - def textparse(ts) - if ts.length < 0 - raise Error, 'empty' - end - - typ, _, val = ts[0] - case typ - when '{' then objparse(ts) - when '[' then arrparse(ts) - else - raise Error, "unexpected #{val.inspect}" - end - end - - - # Parses a "value" in the sense of RFC 4627. - # Returns the parsed value and any trailing tokens. - def valparse(ts) - if ts.length < 0 - raise Error, 'empty' - end - - typ, _, val = ts[0] - case typ - when '{' then objparse(ts) - when '[' then arrparse(ts) - when :val,:str then [val, ts[1..-1]] - else - raise Error, "unexpected #{val.inspect}" - end - end - - - # Parses an "object" in the sense of RFC 4627. - # Returns the parsed value and any trailing tokens. - def objparse(ts) - ts = eat('{', ts) - obj = {} - - if ts[0][0] == '}' - return obj, ts[1..-1] - end - - k, v, ts = pairparse(ts) - obj[k] = v - - if ts[0][0] == '}' - return obj, ts[1..-1] - end - - loop do - ts = eat(',', ts) - - k, v, ts = pairparse(ts) - obj[k] = v - - if ts[0][0] == '}' - return obj, ts[1..-1] - end - end - end - - - # Parses a "member" in the sense of RFC 4627. - # Returns the parsed values and any trailing tokens. - def pairparse(ts) - (typ, _, k), ts = ts[0], ts[1..-1] - if typ != :str - raise Error, "unexpected #{k.inspect}" - end - ts = eat(':', ts) - v, ts = valparse(ts) - [k, v, ts] - end - - - # Parses an "array" in the sense of RFC 4627. - # Returns the parsed value and any trailing tokens. - def arrparse(ts) - ts = eat('[', ts) - arr = [] - - if ts[0][0] == ']' - return arr, ts[1..-1] - end - - v, ts = valparse(ts) - arr << v - - if ts[0][0] == ']' - return arr, ts[1..-1] - end - - loop do - ts = eat(',', ts) - - v, ts = valparse(ts) - arr << v - - if ts[0][0] == ']' - return arr, ts[1..-1] - end - end - end - - - def eat(typ, ts) - if ts[0][0] != typ - raise Error, "expected #{typ} (got #{ts[0].inspect})" - end - ts[1..-1] - end - - - # Scans s and returns a list of json tokens, - # excluding white space (as defined in RFC 4627). - def lex(s) - ts = [] - while s.length > 0 - typ, lexeme, val = tok(s) - if typ == nil - raise Error, "invalid character at #{s[0,10].inspect}" - end - if typ != :space - ts << [typ, lexeme, val] - end - s = s[lexeme.length..-1] - end - ts - end - - - # Scans the first token in s and - # returns a 3-element list, or nil - # if s does not begin with a valid token. - # - # The first list element is one of - # '{', '}', ':', ',', '[', ']', - # :val, :str, and :space. - # - # The second element is the lexeme. - # - # The third element is the value of the - # token for :val and :str, otherwise - # it is the lexeme. - def tok(s) - case s[0] - when ?{ then ['{', s[0,1], s[0,1]] - when ?} then ['}', s[0,1], s[0,1]] - when ?: then [':', s[0,1], s[0,1]] - when ?, then [',', s[0,1], s[0,1]] - when ?[ then ['[', s[0,1], s[0,1]] - when ?] then [']', s[0,1], s[0,1]] - when ?n then nulltok(s) - when ?t then truetok(s) - when ?f then falsetok(s) - when ?" then strtok(s) - when Spc then [:space, s[0,1], s[0,1]] - when ?\t then [:space, s[0,1], s[0,1]] - when ?\n then [:space, s[0,1], s[0,1]] - when ?\r then [:space, s[0,1], s[0,1]] - else numtok(s) - end - end - - - def nulltok(s); s[0,4] == 'null' ? [:val, 'null', nil] : [] end - def truetok(s); s[0,4] == 'true' ? [:val, 'true', true] : [] end - def falsetok(s); s[0,5] == 'false' ? [:val, 'false', false] : [] end - - - def numtok(s) - m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s) - if m && m.begin(0) == 0 - if m[3] && !m[2] - [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))] - elsif m[2] - [:val, m[0], Float(m[0])] - else - [:val, m[0], Integer(m[0])] - end - else - [] - end - end - - - def strtok(s) - m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s) - if ! m - raise Error, "invalid string literal at #{abbrev(s)}" - end - [:str, m[0], unquote(m[0])] - end - - - def abbrev(s) - t = s[0,10] - p = t['`'] - t = t[0,p] if p - t = t + '...' if t.length < s.length - '`' + t + '`' - end - - - # Converts a quoted json string literal q into a UTF-8-encoded string. - # The rules are different than for Ruby, so we cannot use eval. - # Unquote will raise an error if q contains control characters. - def unquote(q) - q = q[1...-1] - rubydoesenc = false - # In ruby >= 1.9, a[w] is a codepoint, not a byte. - if q.class.method_defined?(:force_encoding) - q.force_encoding('UTF-8') - rubydoesenc = true - end - a = q.dup # allocate a big enough string - r, w = 0, 0 - while r < q.length - c = q[r] - case true - when c == ?\\ - r += 1 - if r >= q.length - raise Error, "string literal ends with a \"\\\": \"#{q}\"" - end - - case q[r] - when ?",?\\,?/,?' - a[w] = q[r] - r += 1 - w += 1 - when ?b,?f,?n,?r,?t - a[w] = Unesc[q[r]] - r += 1 - w += 1 - when ?u - r += 1 - uchar = begin - hexdec4(q[r,4]) - rescue RuntimeError => e - raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}" - end - r += 4 - if surrogate? uchar - if q.length >= r+6 - uchar1 = hexdec4(q[r+2,4]) - uchar = subst(uchar, uchar1) - if uchar != Ucharerr - # A valid pair; consume. - r += 6 - end - end - end - if rubydoesenc - a[w] = '' << uchar - w += 1 - else - w += ucharenc(a, w, uchar) - end - else - raise Error, "invalid escape char #{q[r]} in \"#{q}\"" - end - when c == ?", c < Spc - raise Error, "invalid character in string literal \"#{q}\"" - else - # Copy anything else byte-for-byte. - # Valid UTF-8 will remain valid UTF-8. - # Invalid UTF-8 will remain invalid UTF-8. - # In ruby >= 1.9, c is a codepoint, not a byte, - # in which case this is still what we want. - a[w] = c - r += 1 - w += 1 - end - end - a[0,w] - end - - - # Encodes unicode character u as UTF-8 - # bytes in string a at position i. - # Returns the number of bytes written. - def ucharenc(a, i, u) - case true - when u <= Uchar1max - a[i] = (u & 0xff).chr - 1 - when u <= Uchar2max - a[i+0] = (Utag2 | ((u>>6)&0xff)).chr - a[i+1] = (Utagx | (u&Umaskx)).chr - 2 - when u <= Uchar3max - a[i+0] = (Utag3 | ((u>>12)&0xff)).chr - a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr - a[i+2] = (Utagx | (u&Umaskx)).chr - 3 - else - a[i+0] = (Utag4 | ((u>>18)&0xff)).chr - a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr - a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr - a[i+3] = (Utagx | (u&Umaskx)).chr - 4 - end - end - - - def hexdec4(s) - if s.length != 4 - raise Error, 'short' - end - (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3]) - end - - - def subst(u1, u2) - if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3 - return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself - end - return Ucharerr - end - - - def surrogate?(u) - Usurr1 <= u && u < Usurr3 - end - - - def nibble(c) - case true - when ?0 <= c && c <= ?9 then c.ord - ?0.ord - when ?a <= c && c <= ?z then c.ord - ?a.ord + 10 - when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10 - else - raise Error, "invalid hex code #{c}" - end - end - - - # Encodes x into a json text. It may contain only - # Array, Hash, String, Numeric, true, false, nil. - # (Note, this list excludes Symbol.) - # X itself must be an Array or a Hash. - # No other value can be encoded, and an error will - # be raised if x contains any other value, such as - # Nan, Infinity, Symbol, and Proc, or if a Hash key - # is not a String. - # Strings contained in x must be valid UTF-8. - def encode(x) - case x - when Hash then objenc(x) - when Array then arrenc(x) - else - raise Error, 'root value must be an Array or a Hash' - end - end - - - def valenc(x) - case x - when Hash then objenc(x) - when Array then arrenc(x) - when String then strenc(x) - when Numeric then numenc(x) - when true then "true" - when false then "false" - when nil then "null" - else - raise Error, "cannot encode #{x.class}: #{x.inspect}" - end - end - - - def objenc(x) - '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}' - end - - - def arrenc(a) - '[' + a.map{|x| valenc(x)}.join(',') + ']' - end - - - def keyenc(k) - case k - when String then strenc(k) - else - raise Error, "Hash key is not a string: #{k.inspect}" - end - end - - - def strenc(s) - t = StringIO.new - t.putc(?") - r = 0 - - # In ruby >= 1.9, s[r] is a codepoint, not a byte. - rubydoesenc = s.class.method_defined?(:encoding) - - while r < s.length - case s[r] - when ?" then t.print('\\"') - when ?\\ then t.print('\\\\') - when ?\b then t.print('\\b') - when ?\f then t.print('\\f') - when ?\n then t.print('\\n') - when ?\r then t.print('\\r') - when ?\t then t.print('\\t') - else - c = s[r] - case true - when rubydoesenc - begin - c.ord # will raise an error if c is invalid UTF-8 - t.write(c) - rescue - t.write(Ustrerr) - end - when Spc <= c && c <= ?~ - t.putc(c) - else - n = ucharcopy(t, s, r) # ensure valid UTF-8 output - r += n - 1 # r is incremented below - end - end - r += 1 - end - t.putc(?") - t.string - end - - - def numenc(x) - if ((x.nan? || x.infinite?) rescue false) - raise Error, "Numeric cannot be represented: #{x}" - end - "#{x}" - end - - - # Copies the valid UTF-8 bytes of a single character - # from string s at position i to I/O object t, and - # returns the number of bytes copied. - # If no valid UTF-8 char exists at position i, - # ucharcopy writes Ustrerr and returns 1. - def ucharcopy(t, s, i) - n = s.length - i - raise Utf8Error if n < 1 - - c0 = s[i].ord - - # 1-byte, 7-bit sequence? - if c0 < Utagx - t.putc(c0) - return 1 - end - - raise Utf8Error if c0 < Utag2 # unexpected continuation byte? - - raise Utf8Error if n < 2 # need continuation byte - c1 = s[i+1].ord - raise Utf8Error if c1 < Utagx || Utag2 <= c1 - - # 2-byte, 11-bit sequence? - if c0 < Utag3 - raise Utf8Error if ((c0&Umask2)<<6 | (c1&Umaskx)) <= Uchar1max - t.putc(c0) - t.putc(c1) - return 2 - end - - # need second continuation byte - raise Utf8Error if n < 3 - - c2 = s[i+2].ord - raise Utf8Error if c2 < Utagx || Utag2 <= c2 - - # 3-byte, 16-bit sequence? - if c0 < Utag4 - u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx) - raise Utf8Error if u <= Uchar2max - t.putc(c0) - t.putc(c1) - t.putc(c2) - return 3 - end - - # need third continuation byte - raise Utf8Error if n < 4 - c3 = s[i+3].ord - raise Utf8Error if c3 < Utagx || Utag2 <= c3 - - # 4-byte, 21-bit sequence? - if c0 < Utag5 - u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx) - raise Utf8Error if u <= Uchar3max - t.putc(c0) - t.putc(c1) - t.putc(c2) - t.putc(c3) - return 4 - end - - raise Utf8Error - rescue Utf8Error - t.write(Ustrerr) - return 1 - end - - - class Utf8Error < ::StandardError - end - - - class Error < ::StandardError - end - - - Utagx = 0x80 # 1000 0000 - Utag2 = 0xc0 # 1100 0000 - Utag3 = 0xe0 # 1110 0000 - Utag4 = 0xf0 # 1111 0000 - Utag5 = 0xF8 # 1111 1000 - Umaskx = 0x3f # 0011 1111 - Umask2 = 0x1f # 0001 1111 - Umask3 = 0x0f # 0000 1111 - Umask4 = 0x07 # 0000 0111 - Uchar1max = (1<<7) - 1 - Uchar2max = (1<<11) - 1 - Uchar3max = (1<<16) - 1 - Ucharerr = 0xFFFD # unicode "replacement char" - Ustrerr = "\xef\xbf\xbd" # unicode "replacement char" - Usurrself = 0x10000 - Usurr1 = 0xd800 - Usurr2 = 0xdc00 - Usurr3 = 0xe000 - - Spc = ' '[0] - Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t} - end -end diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index ab59a054f..590bf313f 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -35,7 +35,7 @@ module Heroku::Command :path => %r{^/apps/example/addons$} }, { - :body => Heroku::OkJson.encode([ + :body => MultiJson.dump([ { 'configured' => false, 'name' => 'deployhooks:email' }, { 'attachment_name' => 'HEROKU_POSTGRESQL_RED', 'configured' => true, 'name' => 'heroku-postgresql:ronin' }, { 'configured' => true, 'name' => 'deployhooks:http' } @@ -63,7 +63,7 @@ module Heroku::Command it "sends region option to the server" do stub_request(:get, %r{/addons\?region=eu$}). - to_return(:body => Heroku::OkJson.encode([])) + to_return(:body => MultiJson.dump([])) execute("addons:list --region=eu") end @@ -104,7 +104,7 @@ module Heroku::Command it "gives a deprecation notice with an example" do stub_request(:post, %r{apps/example/addons/my_addon$}). with(:body => {:config => {:foo => 'bar', :extra => "XXX"}}). - to_return(:body => Heroku::OkJson.encode({ 'price' => 'free' })) + to_return(:body => MultiJson.dump({ 'price' => 'free' })) Excon.stub( { :expects => 200, @@ -112,7 +112,7 @@ module Heroku::Command :path => %r{^/apps/example/releases/current} }, { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), + :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) @@ -244,7 +244,7 @@ module Heroku::Command :path => %r{^/apps/example/releases/current} }, { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), + :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) @@ -322,7 +322,7 @@ module Heroku::Command :path => %r{^/apps/example/releases/current} }, { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), + :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) @@ -380,7 +380,7 @@ module Heroku::Command :path => %r{^/apps/example/releases/current} }, { - :body => Heroku::OkJson.encode({ 'name' => 'v99' }), + :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) @@ -487,7 +487,7 @@ module Heroku::Command :path => %r{^/addons$} }, { - :body => Heroku::OkJson.encode([ + :body => MultiJson.dump([ { 'name' => 'qux:foo' }, { 'name' => 'quux:bar' } ]), @@ -569,7 +569,7 @@ module Heroku::Command :path => %r{^/apps/example/addons$} }, { - :body => Heroku::OkJson.encode([ + :body => MultiJson.dump([ { 'name' => 'deployhooks:email' }, { 'name' => 'deployhooks:http' } ]), diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index b598f4679..53ca9f676 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -191,7 +191,7 @@ module Heroku::Command context("index with orgs") do context("when you are a member of the org") do before(:each) do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, { :status => 200, :body => Heroku::OkJson.encode({ + Excon.stub({ :method => :get, :path => '/v1/user/info' }, { :status => 200, :body => MultiJson.dump({ "user" => {"default_organization" => "test-org"} })}) end @@ -201,7 +201,7 @@ module Heroku::Command end it "displays a message when the org has no apps" do - Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { :status => 200, :body => Heroku::OkJson.encode([]) }) + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { :status => 200, :body => MultiJson.dump([]) }) stderr, stdout = execute("apps") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT @@ -214,7 +214,7 @@ module Heroku::Command before(:each) do Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { - :body => Heroku::OkJson.encode([ + :body => MultiJson.dump([ {"name" => "org-app-1", "joined" => true}, {"name" => "org-app-2"} ]), diff --git a/spec/heroku/command/orgs_spec.rb b/spec/heroku/command/orgs_spec.rb index 7f60db918..e39321638 100644 --- a/spec/heroku/command/orgs_spec.rb +++ b/spec/heroku/command/orgs_spec.rb @@ -25,7 +25,7 @@ module Heroku::Command it "lists orgs with roles that the user belongs to" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => Heroku::OkJson.encode({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {}}), + :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {}}), :status => 200 } ) @@ -42,7 +42,7 @@ module Heroku::Command it "labels a user's default organization" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => Heroku::OkJson.encode({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {"default_organization" => "test-org2"}}), + :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {"default_organization" => "test-org2"}}), :status => 200 } ) @@ -92,7 +92,7 @@ module Heroku::Command it "displays the default organization when present" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => Heroku::OkJson.encode({"user" => {"default_organization" => "test-org"}}), + :body => MultiJson.dump({"user" => {"default_organization" => "test-org"}}), :status => 200 } ) @@ -130,7 +130,7 @@ module Heroku::Command it "opens the default org" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => Heroku::OkJson.encode({"organizations" => [{"organization_name" => "test-org"}], "user" => {"default_organization" => "test-org"}}), + :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org"}], "user" => {"default_organization" => "test-org"}}), :status => 200 } ) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 8ee5ce971..7bff764c9 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -232,7 +232,7 @@ module Heroku::Command stub(pgc).color? { false } end Excon.stub({:method => :post, :path => '/reports'}, { - :body => Heroku::OkJson.encode({ + :body => MultiJson.dump({ 'id' => 'abc123', 'app' => 'appname', 'created_at' => '2014-06-24 01:26:11.941197+00', diff --git a/spec/heroku/command/status_spec.rb b/spec/heroku/command/status_spec.rb index 01b0ea331..d3d02b0d2 100644 --- a/spec/heroku/command/status_spec.rb +++ b/spec/heroku/command/status_spec.rb @@ -16,7 +16,7 @@ module Heroku::Command :path => '/api/v3/current-status.json' }, { - :body => Heroku::OkJson.encode({"status"=>{"Production"=>"red", "Development"=>"red"}, "issues"=>[{"created_at"=>"2012-06-07T15:55:51Z", "id"=>372, "resolved"=>false, "title"=>"HTTP Routing Errors", "updated_at"=>"2012-06-07T16:14:37Z", "href"=>"https://status.heroku.com/api/v3/issues/372", "updates"=>[{"contents"=>"The number of applications seeing H99 errors is continuing to decrease as we continue to work toward a full resolution of the HTTP routing issues. The API is back online now as well. ", "created_at"=>"2012-06-07T17:47:26Z", "id"=>1088, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:47:26Z"}, {"contents"=>"Our engineers are continuing to work toward a full resolution of the HTTP routing issues. The API is currently in maintenance mode intentionally as we restore application operations. ", "created_at"=>"2012-06-07T17:16:40Z", "id"=>1086, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:26:55Z"}, {"contents"=>"Most applications are back online at this time. Our engineers are working on getting the remaining apps back online. ", "created_at"=>"2012-06-07T16:50:21Z", "id"=>1085, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:50:21Z"}, {"contents"=>"Our routing engineers have pushed out a patch to our routing tier. The platform is recovering and applications are coming back online. Our engineers are continuing to fully restore service.", "created_at"=>"2012-06-07T16:36:37Z", "id"=>1084, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:36:37Z"}, {"contents"=>"We have identified an issue with our routers that is causing errors on HTTP requests to applications. Engineers are working to resolve the issue.\r\n", "created_at"=>"2012-06-07T16:15:25Z", "id"=>1083, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:15:28Z"}, {"contents"=>"We have confirmed widespread errors on the platform. Our engineers are continuing to investigate.\r\n", "created_at"=>"2012-06-07T15:58:56Z", "id"=>1082, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T15:58:58Z"}, {"contents"=>"Our automated systems have detected potential platform errors. We are investigating.\r\n", "created_at"=>"2012-06-07T15:55:51Z", "id"=>1081, "incident_id"=>372, "status_dev"=>"yellow", "status_prod"=>"yellow", "update_type"=>"issue", "updated_at"=>"2012-06-07T15:55:55Z"}]}]}), + :body => MultiJson.dump({"status"=>{"Production"=>"red", "Development"=>"red"}, "issues"=>[{"created_at"=>"2011-06-07T15:55:51Z", "id"=>372, "resolved"=>false, "title"=>"HTTP Routing Errors", "updated_at"=>"2012-06-07T16:14:37Z", "href"=>"https://status.heroku.com/api/v3/issues/372", "updates"=>[{"contents"=>"The number of applications seeing H99 errors is continuing to decrease as we continue to work toward a full resolution of the HTTP routing issues. The API is back online now as well. ", "created_at"=>"2012-06-07T17:47:26Z", "id"=>1088, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:47:26Z"}, {"contents"=>"Our engineers are continuing to work toward a full resolution of the HTTP routing issues. The API is currently in maintenance mode intentionally as we restore application operations. ", "created_at"=>"2012-06-07T17:16:40Z", "id"=>1086, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:26:55Z"}, {"contents"=>"Most applications are back online at this time. Our engineers are working on getting the remaining apps back online. ", "created_at"=>"2012-06-07T16:50:21Z", "id"=>1085, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:50:21Z"}, {"contents"=>"Our routing engineers have pushed out a patch to our routing tier. The platform is recovering and applications are coming back online. Our engineers are continuing to fully restore service.", "created_at"=>"2012-06-07T16:36:37Z", "id"=>1084, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:36:37Z"}, {"contents"=>"We have identified an issue with our routers that is causing errors on HTTP requests to applications. Engineers are working to resolve the issue.\r\n", "created_at"=>"2012-06-07T16:15:25Z", "id"=>1083, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:15:28Z"}, {"contents"=>"We have confirmed widespread errors on the platform. Our engineers are continuing to investigate.\r\n", "created_at"=>"2012-06-07T15:58:56Z", "id"=>1082, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T15:58:58Z"}, {"contents"=>"Our automated systems have detected potential platform errors. We are investigating.\r\n", "created_at"=>"2012-06-07T15:55:51Z", "id"=>1081, "incident_id"=>372, "status_dev"=>"yellow", "status_prod"=>"yellow", "update_type"=>"issue", "updated_at"=>"2012-06-07T15:55:55Z"}]}]}), :status => 200 } ) From d17dcedf02fb61e2c9030020bd1e200a0170086e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 14 Oct 2014 19:20:44 -0700 Subject: [PATCH 097/952] removed unused rake command --- Rakefile | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/Rakefile b/Rakefile index c759e163a..29ca58b07 100644 --- a/Rakefile +++ b/Rakefile @@ -135,26 +135,6 @@ Dir[File.expand_path("../dist/**/*.rake", __FILE__)].each do |rake| import rake end -def poll_ci - require("net/http") - data = Heroku::OkJson.decode(Net::HTTP.get("travis-ci.org", "/heroku/heroku.json")) - case data["last_build_status"] - when nil - print(".") - sleep(1) - poll_ci - when 0 - puts("SUCCESS") - when 1 - puts("FAILURE") - end -end - -desc("Check current ci status and/or wait for build to finish.") -task "ci" do - poll_ci -end - desc("Create a new changelog article") task "changelog" do changelog = <<-CHANGELOG From 81bcb0d1dfdb885fb1ec9b416103b527818c2656 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 15 Oct 2014 11:48:11 -0700 Subject: [PATCH 098/952] Do not prompt for git remote update --- lib/heroku/command/git.rb | 9 +-------- spec/heroku/command/git_spec.rb | 13 ++----------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 4e0d9ce6c..e72f6cf6c 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -46,7 +46,6 @@ def clone # if OPTIONS are specified they will be passed to git remote add # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --update # update the remote if it already exists # --http-git # HIDDEN: Use HTTP git protocol # #Examples: @@ -54,9 +53,6 @@ def clone # $ heroku git:remote -a example # Git remote heroku added # - # $ heroku git:remote -a example - # Git remote heroku already exists. Would you like to update it? (y/n) - # def remote remote = options[:remote] || 'heroku' app_data = api.get_app(app).body @@ -67,10 +63,7 @@ def remote end if git('remote').split("\n").include?(remote) - q = "Git remote #{remote} already exists. Would you like to update it? (y/n)" - if options[:update] || confirm(q) - update_git_remote(remote, git_url) - end + update_git_remote(remote, git_url) else create_git_remote(remote, git_url) end diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb index 876c30306..132938e7e 100644 --- a/spec/heroku/command/git_spec.rb +++ b/spec/heroku/command/git_spec.rb @@ -127,26 +127,17 @@ module Heroku::Command STDOUT end - it "updates remote when it already exists if update flag is set" do + it "updates remote when it already exists" do any_instance_of(Heroku::Command::Git) do |git| stub(git).git('remote').returns("heroku") stub(git).git('remote set-url heroku git@heroku.com:example.git') end - stderr, stdout = execute("git:remote --update") + stderr, stdout = execute("git:remote") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Git remote heroku updated STDOUT end - - it "prompts to updates remote when it already exists if update flag is not set" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('remote').returns("heroku") - end - stderr, stdout = execute("git:remote") - expect(stderr).to eq("") - expect(stdout).to eq "Git remote heroku already exists. Would you like to update it? (y/n) " - end end end end From f572b99996caa0c510cb51069d600b62cb59ce18 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 15 Oct 2014 11:51:36 -0700 Subject: [PATCH 099/952] Extract DEFAULT_REMOTE --- lib/heroku/command/git.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index e72f6cf6c..aa75eac9f 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -4,6 +4,8 @@ # class Heroku::Command::Git < Heroku::Command::Base + DEFAULT_REMOTE = 'heroku' + # git:clone APP [DIRECTORY] # # clones a heroku app to your local machine at DIRECTORY (defaults to app name) @@ -21,7 +23,7 @@ class Heroku::Command::Git < Heroku::Command::Base # ... # def clone - remote = options[:remote] || "heroku" + remote = options[:remote] || DEFAULT_REMOTE name = options[:app] || shift_argument || error("Usage: heroku git:clone APP [DIRECTORY]") directory = shift_argument @@ -54,7 +56,7 @@ def clone # Git remote heroku added # def remote - remote = options[:remote] || 'heroku' + remote = options[:remote] || DEFAULT_REMOTE app_data = api.get_app(app).body git_url = if options[:http_git] "https://#{Heroku::Auth.http_git_host}/#{app_data['name']}.git" From 90805b4e1d08419869fb693ae914d3ad6518b5ad Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 15 Oct 2014 11:56:52 -0700 Subject: [PATCH 100/952] Extract git_url method --- lib/heroku/command/git.rb | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index aa75eac9f..f568ba874 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -29,12 +29,6 @@ def clone directory = shift_argument validate_arguments! - git_url = if options[:http_git] - "https://#{Heroku::Auth.http_git_host}/#{name}.git" - else - api.get_app(name).body["git_url"] - end - puts "Cloning from app '#{name}'..." system "git clone -o #{remote} #{git_url} #{directory}".strip end @@ -57,17 +51,21 @@ def clone # def remote remote = options[:remote] || DEFAULT_REMOTE - app_data = api.get_app(app).body - git_url = if options[:http_git] - "https://#{Heroku::Auth.http_git_host}/#{app_data['name']}.git" - else - app_data['git_url'] - end - if git('remote').split("\n").include?(remote) update_git_remote(remote, git_url) else create_git_remote(remote, git_url) end end + + private + + def git_url + app_info = api.get_app(app).body + if options[:http_git] + "https://#{Heroku::Auth.http_git_host}/#{app_info['name']}.git" + else + app_info['git_url'] + end + end end From 12e238211e7af0ab1d17826debe7b4fe11022129 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 15 Oct 2014 12:02:51 -0700 Subject: [PATCH 101/952] Extract remote_name method --- lib/heroku/command/git.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index f568ba874..466f42643 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -4,7 +4,7 @@ # class Heroku::Command::Git < Heroku::Command::Base - DEFAULT_REMOTE = 'heroku' + DEFAULT_REMOTE_NAME = 'heroku' # git:clone APP [DIRECTORY] # @@ -23,14 +23,12 @@ class Heroku::Command::Git < Heroku::Command::Base # ... # def clone - remote = options[:remote] || DEFAULT_REMOTE - name = options[:app] || shift_argument || error("Usage: heroku git:clone APP [DIRECTORY]") directory = shift_argument validate_arguments! puts "Cloning from app '#{name}'..." - system "git clone -o #{remote} #{git_url} #{directory}".strip + system "git clone -o #{remote_name} #{git_url} #{directory}".strip end alias_command "clone", "git:clone" @@ -50,16 +48,19 @@ def clone # Git remote heroku added # def remote - remote = options[:remote] || DEFAULT_REMOTE - if git('remote').split("\n").include?(remote) - update_git_remote(remote, git_url) + if git('remote').split("\n").include?(remote_name) + update_git_remote(remote_name, git_url) else - create_git_remote(remote, git_url) + create_git_remote(remote_name, git_url) end end private + def remote_name + options[:remote] || DEFAULT_REMOTE_NAME + end + def git_url app_info = api.get_app(app).body if options[:http_git] From ad91d0cdeabe12de0a766ec0c083a1dd046a675f Mon Sep 17 00:00:00 2001 From: Brett Goulder Date: Wed, 15 Oct 2014 13:46:16 -0700 Subject: [PATCH 102/952] Remove legacy SSL --- lib/heroku/command/ssl.rb | 43 --------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 lib/heroku/command/ssl.rb diff --git a/lib/heroku/command/ssl.rb b/lib/heroku/command/ssl.rb deleted file mode 100644 index 1441aaa91..000000000 --- a/lib/heroku/command/ssl.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # DEPRECATED: see `heroku certs` instead - # - # manage ssl certificates for an app - # - class Ssl < Base - - # ssl - # - # list legacy certificates for an app - # - def index - api.get_domains(app).body.each do |domain| - if cert = domain['cert'] - display "#{domain['domain']} has a SSL certificate registered to #{cert['subject']} which expires on #{format_date(cert['expires_at'])}" - else - display "#{domain['domain']} has no certificate" - end - end - end - - # ssl:add PEM KEY - # - # DEPRECATED: see `heroku certs:add` instead - # - def add - $stderr.puts " ! `heroku ssl:add` has been deprecated. Please use the SSL Endpoint add-on and the `heroku certs` commands instead." - $stderr.puts " ! SSL Endpoint documentation is available at: https://devcenter.heroku.com/articles/ssl-endpoint" - end - - # ssl:clear - # - # remove legacy ssl certificates from an app - # - def clear - heroku.clear_ssl(app) - display "Cleared certificates for #{app}" - end - end -end From fb6c6a8eac4b4de1dc752b0560a873f3e9ac72e3 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Fri, 17 Oct 2014 10:34:16 -0700 Subject: [PATCH 103/952] Prompt for 2fa on stderr instead of stdout This means that a 2fa prompt is still visible if, e.g., one is piping the result of a heroku command which requires 2fa into another command. --- lib/heroku/auth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 7f32c48dc..65cc850dd 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -211,7 +211,7 @@ def ask_for_credentials end def ask_for_second_factor - display "Two-factor code: ", false + $stderr.print "Two-factor code: " @two_factor_code = ask @two_factor_code = nil if @two_factor_code == "" @api = nil # reset it From cafe82fb9fe6b1803ceab4f5e14e897258b47bbd Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Fri, 17 Oct 2014 18:24:41 -0700 Subject: [PATCH 104/952] Add a hook for the pg-extras set commands this way the entire method doesn't have to be overwritten --- lib/heroku/command/pg.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 1f9246980..69c46bb47 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -10,6 +10,12 @@ # manage heroku-postgresql databases # class Heroku::Command::Pg < Heroku::Command::Base + module Hooks + extend self + def set_commands(shorthand) + '' + end + end include Heroku::Helpers::HerokuPostgresql include Heroku::Helpers::PgDiagnose @@ -100,10 +106,11 @@ def psql end shorthand = "#{attachment.app}::#{attachment.name.sub(/^HEROKU_POSTGRESQL_/,'').gsub(/\W+/, '-')}" + set_commands = Hooks.set_commands(shorthand) prompt_expr = "#{shorthand}%R%# " prompt_flags = %Q(--set "PROMPT1=#{prompt_expr}" --set "PROMPT2=#{prompt_expr}") puts "---> Connecting to #{attachment.display_name}" - exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{prompt_flags} #{command} #{uri.path[1..-1]}" + exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{set_commands} #{prompt_flags} #{command} #{uri.path[1..-1]}" rescue Errno::ENOENT output_with_bang "The local psql command could not be located" output_with_bang "For help installing psql, see http://devcenter.heroku.com/articles/local-postgresql" From 023c84d15cde5958631b240eeaadec01a3b49031 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 17 Oct 2014 15:08:13 -0700 Subject: [PATCH 105/952] block all commands while updating --- lib/heroku/cli.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 36389c756..ea20c7f8d 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,6 +1,11 @@ load('heroku/helpers.rb') # reload helpers after possible inject_loadpath load('heroku/updater.rb') # reload updater after possible inject_loadpath +if File.exist? Heroku::Updater.updating_lock_path + $stderr.puts "Heroku Toolbelt is currently updating" + exit 1 +end + require "heroku" require "heroku/command" require "heroku/helpers" From 179d67b87f4048ec345c66f38e7803c6b09e15df Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 20 Oct 2014 13:01:09 -0700 Subject: [PATCH 106/952] v3.13.0.pre --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0b39216ad..1934072e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.12.1) + heroku (3.13.0.pre) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 29acbc766..c855a1e67 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.12.1" + VERSION = "3.13.0.pre" end From e4da5b486c9636fb1d208a1f2f5b34dc3a23b663 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 20 Oct 2014 13:05:53 -0700 Subject: [PATCH 107/952] v3.13.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6f12cc5a0..a3a7203d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.13.0 2014-10-20 +================= +Switch to multi_json +Overwrite git remotes with `heroku git:remote` + 3.12.1 2014-10-07 ================= Fixed Excon 0.40.0 in Gemfile diff --git a/Gemfile.lock b/Gemfile.lock index 1934072e4..5b99f5cd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.13.0.pre) + heroku (3.13.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index c855a1e67..983a102ad 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.13.0.pre" + VERSION = "3.13.0" end From 7ad5bb829c160114fe6df00ffc71d150cf428114 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 20 Oct 2014 13:51:20 -0700 Subject: [PATCH 108/952] clear out 2fa creds after preauth --- lib/heroku/auth.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 65cc850dd..6f809a3c6 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -221,6 +221,8 @@ def ask_for_second_factor def preauth if Heroku.app_name api.request(:method => :put, :path => "/apps/#{Heroku.app_name}/pre-authorizations") + @two_factor_code = nil + @api = nil end end From 8a1b6cd060f1e21e19a88f523ed847f75f9b9e05 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 20 Oct 2014 14:06:10 -0700 Subject: [PATCH 109/952] better error message when auth failure with HEROKU_API_KEY --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 7c125d2c4..fd571805f 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -270,7 +270,7 @@ def self.run(cmd, arguments=[]) def self.handle_auth_error(e) if ENV['HEROKU_API_KEY'] - puts "Authentication failure" + puts "Authentication failure with HEROKU_API_KEY" exit 1 end if wrong_two_factor_code?(e) From 2ef400025bab2292fa37124130d9c951963c2eb4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 20 Oct 2014 16:46:08 -0700 Subject: [PATCH 110/952] only perform 2fa preauth if not in login flow --- lib/heroku/auth.rb | 40 ++++++++++++++++++---------------------- lib/heroku/client.rb | 8 ++------ lib/heroku/command.rb | 7 +++---- 3 files changed, 23 insertions(+), 32 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 6f809a3c6..cdb7dc80b 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -9,7 +9,7 @@ class Heroku::Auth class << self include Heroku::Helpers - attr_accessor :credentials, :two_factor_code + attr_accessor :credentials def api @api ||= begin @@ -81,9 +81,18 @@ def password # :nodoc: get_credentials[1] end - def api_key(user = get_credentials[0], password = get_credentials[1]) - api = Heroku::API.new(default_params) + def api_key(user=get_credentials[0], password=get_credentials[1], second_factor=nil) + params = default_params + if second_factor + params[:headers].merge!("Heroku-Two-Factor-Code" => second_factor) + end + api = Heroku::API.new(params) api.post_login(user, password).body["api_key"] + rescue Heroku::API::Errors::Forbidden => e + if e.response.headers.has_key?("Heroku-Two-Factor-Required") + second_factor = ask_for_second_factor + retry + end rescue Heroku::API::Errors::Unauthorized => e id = json_decode(e.response.body)["id"] raise if id != "invalid_two_factor_code" @@ -92,11 +101,6 @@ def api_key(user = get_credentials[0], password = get_credentials[1]) display "Please check your code was typed correctly and that your" display "authenticator's time keeping is accurate." exit 1 - rescue Heroku::API::Errors::Forbidden => e - if e.response.headers.has_key?("Heroku-Two-Factor-Required") - ask_for_second_factor - retry - end end def get_credentials # :nodoc: @@ -212,18 +216,14 @@ def ask_for_credentials def ask_for_second_factor $stderr.print "Two-factor code: " - @two_factor_code = ask - @two_factor_code = nil if @two_factor_code == "" - @api = nil # reset it - preauth + ask end def preauth - if Heroku.app_name - api.request(:method => :put, :path => "/apps/#{Heroku.app_name}/pre-authorizations") - @two_factor_code = nil - @api = nil - end + second_factor = ask_for_second_factor + api.request(:method => :put, + :path => "/apps/#{Heroku.app_name}/pre-authorizations", + :headers => {"Heroku-Two-Factor-Code" => second_factor}) end def ask_for_password_on_windows @@ -369,12 +369,8 @@ def verify_host?(host) def default_params uri = URI.parse(full_host(host)) - headers = { 'User-Agent' => Heroku.user_agent } - if two_factor_code - headers.merge!("Heroku-Two-Factor-Code" => two_factor_code) - end { - :headers => headers, + :headers => {'User-Agent' => Heroku.user_agent}, :host => uri.host, :port => uri.port.to_s, :scheme => uri.scheme, diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 7286713ad..f71bf261a 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -451,7 +451,7 @@ def console(app_name, cmd=nil) else run_console_command("/apps/#{app_name}/console", cmd) end - rescue RestClient::BadGateway => e + rescue RestClient::BadGateway raise(AppCrashed, <<-ERROR) Unable to attach to a dyno to open a console session. Your application may have crashed. @@ -614,10 +614,6 @@ def process(method, uri, extra_headers={}, payload=nil) headers = heroku_headers.merge(extra_headers) args = [method, payload, headers].compact - if Heroku::Auth.two_factor_code - headers.merge!("Heroku-Two-Factor-Code" => Heroku::Auth.two_factor_code) - end - resource_options = default_resource_options_for_uri(uri) begin @@ -625,7 +621,7 @@ def process(method, uri, extra_headers={}, payload=nil) rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError host = URI.parse(realize_full_uri(uri)).host error "Unable to connect to #{host}" - rescue RestClient::SSLCertificateNotVerified => ex + rescue RestClient::SSLCertificateNotVerified host = URI.parse(realize_full_uri(uri)).host error "WARNING: Unable to verify SSL certificate for #{host}\nTo disable SSL verification, run with HEROKU_SSL_VERIFY=disable" end diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index fd571805f..3d015b447 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -244,7 +244,7 @@ def self.run(cmd, arguments=[]) error "API request timed out. Please try again, or contact support@heroku.com if this issue persists." rescue Heroku::API::Errors::Forbidden => e if e.response.headers.has_key?("Heroku-Two-Factor-Required") - Heroku::Auth.ask_for_second_factor + Heroku::Auth.preauth retry else error extract_error(e.response.body) @@ -253,7 +253,7 @@ def self.run(cmd, arguments=[]) error extract_error(e.response.body) rescue RestClient::RequestFailed => e if e.response.code == 403 && e.response.headers.has_key?(:heroku_two_factor_required) - Heroku::Auth.ask_for_second_factor + Heroku::Auth.preauth retry else error extract_error(e.http_body) @@ -272,8 +272,7 @@ def self.handle_auth_error(e) if ENV['HEROKU_API_KEY'] puts "Authentication failure with HEROKU_API_KEY" exit 1 - end - if wrong_two_factor_code?(e) + elsif wrong_two_factor_code?(e) puts "Invalid two-factor code" false else From 1d7ae3ca2c73b1f6458a8533b9fdf7bd509148c6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 21 Oct 2014 11:57:39 -0700 Subject: [PATCH 111/952] v3.14.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a3a7203d8..1bba9ec6b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.14.0 2014-10-21 +================= +Use preauth instead of 2fa for all API calls + 3.13.0 2014-10-20 ================= Switch to multi_json diff --git a/Gemfile.lock b/Gemfile.lock index 5b99f5cd8..5d65ff57c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.13.0) + heroku (3.14.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 983a102ad..5f5254592 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.13.0" + VERSION = "3.14.0" end From d4c803aefc2585580152081b5d831c357b311103 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 21 Oct 2014 15:13:13 -0700 Subject: [PATCH 112/952] only show old version number after update is complete. It read confusing before --- lib/heroku/command/update.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 5a7ca34ba..8bf3d6cce 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -35,9 +35,9 @@ def beta def update_from_url(prerelease) Heroku::Updater.check_disabled! - action("Updating from #{Heroku::VERSION}") do + action("Updating") do if new_version = Heroku::Updater.update(prerelease) - status("updated to #{new_version}") + status("#{Heroku::VERSION} updated to #{new_version}") else status("nothing to update") end From 6d204d88096dfb1590cbb0ed7bb41d6d36038cc3 Mon Sep 17 00:00:00 2001 From: Bryan Stenson Date: Wed, 22 Oct 2014 08:46:31 -0700 Subject: [PATCH 113/952] don't rescue Exception class...too broad --- lib/heroku/command/certs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index df9db62b6..894091e06 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -198,7 +198,7 @@ def post_to_ssl_doctor(path, action_text = nil) input = args.map { |arg| begin certbody=File.read(arg) - rescue Exception => e + rescue => e error("Unable to read #{arg} file: #{e}") end certbody From e532822b65e5c6d0dee95127c4758fc704fc45ae Mon Sep 17 00:00:00 2001 From: Bryan Stenson Date: Wed, 22 Oct 2014 21:39:41 -0700 Subject: [PATCH 114/952] (more) do not rescue Exception class...too broad --- lib/heroku/auth.rb | 2 +- lib/heroku/command.rb | 2 +- lib/heroku/command/fork.rb | 2 +- lib/heroku/command/run.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index cdb7dc80b..ed7246eaf 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -266,7 +266,7 @@ def ask_for_and_save_credentials display "Authentication failed." retry if retry_login? exit 1 - rescue Exception => e + rescue => e delete_credentials raise e end diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 3d015b447..73cebc495 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -295,7 +295,7 @@ def self.parse_error_xml(body) xml_errors = REXML::Document.new(body).elements.to_a("//errors/error") msg = xml_errors.map { |a| a.text }.join(" / ") return msg unless msg.empty? - rescue Exception + rescue end def self.parse_error_json(body) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index b38d2161e..e12e51bc0 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -88,7 +88,7 @@ def index end puts "Fork complete, view it at #{to_info['web_url']}" - rescue Exception => e + rescue => e raise if e.is_a?(Heroku::Command::CommandFailed) puts "Failed to fork app #{from} to #{to}." diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index ae4b2f43a..92c1d238f 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -174,7 +174,7 @@ def console_history_read(app) end history.each { |cmd| Readline::HISTORY.push(cmd) } rescue Errno::ENOENT - rescue Exception => ex + rescue => ex display "Error reading your console history: #{ex.message}" if confirm("Would you like to clear it? (y/N):") FileUtils.rm(console_history_file(app)) rescue nil From 5de75b67e1495323c35619cfbd3d5c778836df1d Mon Sep 17 00:00:00 2001 From: Bryan Stenson Date: Wed, 22 Oct 2014 21:39:41 -0700 Subject: [PATCH 115/952] (more) do not rescue Exception class...too broad --- lib/heroku/auth.rb | 2 +- lib/heroku/command.rb | 2 +- lib/heroku/command/fork.rb | 2 +- lib/heroku/command/run.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index cdb7dc80b..ed7246eaf 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -266,7 +266,7 @@ def ask_for_and_save_credentials display "Authentication failed." retry if retry_login? exit 1 - rescue Exception => e + rescue => e delete_credentials raise e end diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 3d015b447..73cebc495 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -295,7 +295,7 @@ def self.parse_error_xml(body) xml_errors = REXML::Document.new(body).elements.to_a("//errors/error") msg = xml_errors.map { |a| a.text }.join(" / ") return msg unless msg.empty? - rescue Exception + rescue end def self.parse_error_json(body) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index b38d2161e..e12e51bc0 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -88,7 +88,7 @@ def index end puts "Fork complete, view it at #{to_info['web_url']}" - rescue Exception => e + rescue => e raise if e.is_a?(Heroku::Command::CommandFailed) puts "Failed to fork app #{from} to #{to}." diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index ae4b2f43a..92c1d238f 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -174,7 +174,7 @@ def console_history_read(app) end history.each { |cmd| Readline::HISTORY.push(cmd) } rescue Errno::ENOENT - rescue Exception => ex + rescue => ex display "Error reading your console history: #{ex.message}" if confirm("Would you like to clear it? (y/N):") FileUtils.rm(console_history_file(app)) rescue nil From ce2618fc5b162393e6eb0bcb8b66e5b7ddd6501b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 24 Oct 2014 11:17:35 -0700 Subject: [PATCH 116/952] only perform preauth with an app name --- lib/heroku/auth.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index cdb7dc80b..2b3457685 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -220,10 +220,12 @@ def ask_for_second_factor end def preauth - second_factor = ask_for_second_factor - api.request(:method => :put, - :path => "/apps/#{Heroku.app_name}/pre-authorizations", - :headers => {"Heroku-Two-Factor-Code" => second_factor}) + if Heroku.app_name + second_factor = ask_for_second_factor + api.request(:method => :put, + :path => "/apps/#{Heroku.app_name}/pre-authorizations", + :headers => {"Heroku-Two-Factor-Code" => second_factor}) + end end def ask_for_password_on_windows From 2e4de28f27c4dd47bc4adf8324922bae06a1107f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 24 Oct 2014 11:20:22 -0700 Subject: [PATCH 117/952] v3.15.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1bba9ec6b..38a01601c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.15.0 2014-10-24 +================= +Skip preauth with no app context +Fixed Debian control file dependencies + 3.14.0 2014-10-21 ================= Use preauth instead of 2fa for all API calls diff --git a/Gemfile.lock b/Gemfile.lock index 5d65ff57c..cd0afb8bd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.14.0) + heroku (3.15.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) @@ -38,7 +38,7 @@ GEM addressable (~> 2.3) mime-types (1.25.1) multi_json (1.10.1) - netrc (0.7.7) + netrc (0.7.9) ocra (1.3.2) rake (10.3.2) rest-client (1.6.7) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 5f5254592..39249a56a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.14.0" + VERSION = "3.15.0" end From c8e25f7c9398bb9e155f15e2836d607f09e9a81b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 27 Oct 2014 11:17:23 -0700 Subject: [PATCH 118/952] better messaging when updating --- lib/heroku/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index ea20c7f8d..a886e3802 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -2,7 +2,7 @@ load('heroku/updater.rb') # reload updater after possible inject_loadpath if File.exist? Heroku::Updater.updating_lock_path - $stderr.puts "Heroku Toolbelt is currently updating" + $stderr.puts "Heroku Toolbelt is currently updating. Please wait a few seconds and try your command again." exit 1 end From cafe4912983b531ae6707ec54c593caab9eb5024 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Mon, 3 Nov 2014 13:03:20 -0800 Subject: [PATCH 119/952] Mask 2fa inputs Masks after 12 characters to preserve identifier. Not sure if it works on windows, so keep the old behavior on windows --- lib/heroku/auth.rb | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 2b3457685..f230b2f1a 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -216,7 +216,26 @@ def ask_for_credentials def ask_for_second_factor $stderr.print "Two-factor code: " - ask + running_on_windows? ? ask : ask_for_second_factor_masked + end + + def ask_for_second_factor_masked + state = `stty -g` + `stty raw -echo -icanon isig` + + i = 0 + code = "" + while c = STDIN.getc.chr + break if "\r" == c + i += 1 + code += c + print i < 13 ? c : '.' + end + print "\r\n" + + code + ensure + `stty #{state}` end def preauth From 8a81046cb9e387f9681e4f983eb8a83816c86c46 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 3 Nov 2014 20:45:37 -0800 Subject: [PATCH 120/952] corrected docs for heroku update commands --- lib/heroku/command/update.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 8bf3d6cce..7baeb230e 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -12,7 +12,7 @@ class Heroku::Command::Update < Heroku::Command::Base # Example: # # $ heroku update - # Updating from v1.2.3... done, updated to v2.3.4 + # Updating... done, v1.2.3 updated to v2.3.4 # def index validate_arguments! @@ -24,7 +24,7 @@ def index # update to the latest beta client # # $ heroku update - # Updating from v1.2.3... done, updated to v2.3.4.pre + # Updating... done, v1.2.3 updated to v2.3.4.pre # def beta validate_arguments! From 0e5b7f36d8ca7e5de383ff1a122ec3bdce04db9e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 3 Nov 2014 20:46:01 -0800 Subject: [PATCH 121/952] perform check for update outside of lock --- lib/heroku/updater.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 5ff50f675..e940044d5 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -86,13 +86,12 @@ def self.wait_for_lock(wait_for=5, check_every=0.5) end def self.update(prerelease) + return unless prerelease || needs_update? + wait_for_lock do - require "heroku" require "tmpdir" require "zip/zip" - return unless prerelease || needs_update? - Dir.mktmpdir do |download_dir| zip_filename = "#{download_dir}/heroku.zip" if prerelease From 70487a130a8956ca3f77284b91ffb2235fcfddae Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 4 Nov 2014 09:43:08 -0800 Subject: [PATCH 122/952] updated gems --- Gemfile.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cd0afb8bd..48592585d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,45 +29,45 @@ GEM diff-lcs (1.2.5) docile (1.1.5) excon (0.40.0) - fakefs (0.5.2) + fakefs (0.6.0) heroku-api (0.3.19) excon (~> 0.38) multi_json (~> 1.8) json (1.8.1) - launchy (2.4.2) + launchy (2.4.3) addressable (~> 2.3) mime-types (1.25.1) multi_json (1.10.1) netrc (0.7.9) - ocra (1.3.2) + ocra (1.3.3) rake (10.3.2) rest-client (1.6.7) mime-types (>= 1.16) rr (1.1.2) - rspec (3.0.0) - rspec-core (~> 3.0.0) - rspec-expectations (~> 3.0.0) - rspec-mocks (~> 3.0.0) - rspec-core (3.0.4) - rspec-support (~> 3.0.0) - rspec-expectations (3.0.4) + rspec (3.1.0) + rspec-core (~> 3.1.0) + rspec-expectations (~> 3.1.0) + rspec-mocks (~> 3.1.0) + rspec-core (3.1.7) + rspec-support (~> 3.1.0) + rspec-expectations (3.1.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.0.0) - rspec-mocks (3.0.4) - rspec-support (~> 3.0.0) - rspec-support (3.0.4) + rspec-support (~> 3.1.0) + rspec-mocks (3.1.3) + rspec-support (~> 3.1.0) + rspec-support (3.1.2) rubyzip (0.9.9) - safe_yaml (1.0.3) - simplecov (0.9.0) + safe_yaml (1.0.4) + simplecov (0.9.1) docile (~> 1.1.0) - multi_json + multi_json (~> 1.0) simplecov-html (~> 0.8.0) simplecov-html (0.8.0) term-ansicolor (1.3.0) tins (~> 1.0) thor (0.19.1) - tins (1.3.2) - webmock (1.18.0) + tins (1.3.3) + webmock (1.20.2) addressable (>= 2.3.6) crack (>= 0.3.2) xml-simple (1.1.4) From 58d1b67d8e32a1c6f036634e9a3826d70d4d8018 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 4 Nov 2014 10:00:05 -0800 Subject: [PATCH 123/952] v3.15.1 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 38a01601c..b60d0a751 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.15.1 2014-11-04 +================= +Upgraded launchy to 2.4.3 +Only lock for updates when updating, not checking for updates + 3.15.0 2014-10-24 ================= Skip preauth with no app context diff --git a/Gemfile.lock b/Gemfile.lock index 48592585d..a4323e8e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.15.0) + heroku (3.15.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 39249a56a..3cd963371 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.15.0" + VERSION = "3.15.1" end From bfb28e9bec9390f0f5f4d29fd6e27a632e6cb69e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 4 Nov 2014 11:05:40 -0800 Subject: [PATCH 124/952] v3.15.2 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b60d0a751..f1aa6ee69 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.15.2 2014-11-04 +================= +Mask 2fa inputs longer than 12 characters + 3.15.1 2014-11-04 ================= Upgraded launchy to 2.4.3 diff --git a/Gemfile.lock b/Gemfile.lock index a4323e8e8..ffb9dfa78 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.15.1) + heroku (3.15.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3cd963371..153b6bef5 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.15.1" + VERSION = "3.15.2" end From 52bb98bf8d5ac0fcc3082384cf14887b512d6831 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 6 Nov 2014 12:42:16 -0800 Subject: [PATCH 125/952] Revert "Mask 2fa inputs" --- lib/heroku/auth.rb | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index f5147e205..3521f2d0f 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -216,26 +216,7 @@ def ask_for_credentials def ask_for_second_factor $stderr.print "Two-factor code: " - running_on_windows? ? ask : ask_for_second_factor_masked - end - - def ask_for_second_factor_masked - state = `stty -g` - `stty raw -echo -icanon isig` - - i = 0 - code = "" - while c = STDIN.getc.chr - break if "\r" == c - i += 1 - code += c - print i < 13 ? c : '.' - end - print "\r\n" - - code - ensure - `stty #{state}` + ask end def preauth From 6ca1a581886e3c2720c387b1ce968e8f0a02063a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 13:25:44 -0800 Subject: [PATCH 126/952] removed unused ocra windows binary --- Gemfile | 1 - Gemfile.lock | 2 -- appveyor.yml | 16 ---------------- bin/heroku-ocra | 21 --------------------- 4 files changed, 40 deletions(-) delete mode 100644 appveyor.yml delete mode 100644 bin/heroku-ocra diff --git a/Gemfile b/Gemfile index e016c9c38..7025e9ec8 100644 --- a/Gemfile +++ b/Gemfile @@ -12,5 +12,4 @@ group :development, :test do gem "rspec" gem "webmock" gem "coveralls", :require => false - gem "ocra", :require => false end diff --git a/Gemfile.lock b/Gemfile.lock index ffb9dfa78..eb3de3096 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,7 +39,6 @@ GEM mime-types (1.25.1) multi_json (1.10.1) netrc (0.7.9) - ocra (1.3.3) rake (10.3.2) rest-client (1.6.7) mime-types (>= 1.16) @@ -83,7 +82,6 @@ DEPENDENCIES heroku! json mime-types (< 2.0) - ocra rake rr rspec diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 7cd92d822..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "{build}" -branches: - only: - - master -clone_depth: 1 -install: - - ruby --version - - bundle install -j4 -build_script: - - ocra bin\heroku-ocra data\cacert.pem -test_script: - - heroku-ocra.exe help - - heroku-ocra.exe status -artifacts: - - path: heroku-ocra.exe - name: heroku-ocra.exe diff --git a/bin/heroku-ocra b/bin/heroku-ocra deleted file mode 100644 index d464fa12a..000000000 --- a/bin/heroku-ocra +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env ruby -# encoding: UTF-8 - -# resolve bin path, ignoring symlinks -require "pathname" -bin_file = Pathname.new(__FILE__).realpath - -# add self to libpath -$:.unshift File.expand_path("../../lib", bin_file) - -require "heroku/updater" -Heroku::Updater.disable("`heroku update` is only available from Heroku Toolbelt.\nDownload and install from https://toolbelt.heroku.com") - -# start up the CLI -require "heroku/cli" -Heroku.user_agent = "heroku-ocra/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" -Heroku::CLI.start(*ARGV) - -# require other dependencies ocra needs to include -require 'Win32API' -MultiJson.load('{"foo": "bar"}') # preps multi_json From 48104dc2c720c244a0a2f3ea5c6d2487d7f43851 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 13:31:44 -0800 Subject: [PATCH 127/952] downgrade fakefs to be 1.8.7 compatible --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 7025e9ec8..b7dbe2580 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,7 @@ group :development, :test do gem "rr" gem "aws-s3" gem "mime-types", "< 2.0" - gem "fakefs" + gem "fakefs", "< 0.6" gem "json" gem "rspec" gem "webmock" diff --git a/Gemfile.lock b/Gemfile.lock index eb3de3096..12f853733 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -29,7 +29,7 @@ GEM diff-lcs (1.2.5) docile (1.1.5) excon (0.40.0) - fakefs (0.6.0) + fakefs (0.5.2) heroku-api (0.3.19) excon (~> 0.38) multi_json (~> 1.8) @@ -78,7 +78,7 @@ PLATFORMS DEPENDENCIES aws-s3 coveralls - fakefs + fakefs (< 0.6) heroku! json mime-types (< 2.0) From 1238d4b5fa5a6b299c27a11964d585af2efe140f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 13:32:25 -0800 Subject: [PATCH 128/952] reenable email notifications since I missed the build failures --- .travis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4e70d1565..a0c8e510f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,6 @@ before_script: script: bundle exec rspec spec --color --format documentation -notifications: - email: false - deploy: provider: rubygems on: From 479d3cfc2e1eb0e481b210341530d64c88c6cc32 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 13:46:06 -0800 Subject: [PATCH 129/952] removed appveyor since we're not testing on it anymore --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 10bd0ba75..0c29dfabd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ For more about Heroku see . To get started see [![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) -[![Build status](https://ci.appveyor.com/api/projects/status/kv0r2s5eyckpanhr/branch/master)](https://ci.appveyor.com/project/dickeyxxx/heroku/branch/master) [![Coverage Status](https://img.shields.io/coveralls/heroku/heroku.svg)](https://coveralls.io/r/heroku/heroku?branch=master) [![Dependency Status](https://gemnasium.com/heroku/heroku.svg)](https://gemnasium.com/heroku/heroku) From 31e948c425f5fbd0eea9e9ddb732a1a6e589a05b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 13:46:27 -0800 Subject: [PATCH 130/952] removed gemnasium since we can never be updated due to supporting ruby 1.8.7 --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0c29dfabd..12a823886 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ To get started see [![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) [![Coverage Status](https://img.shields.io/coveralls/heroku/heroku.svg)](https://coveralls.io/r/heroku/heroku?branch=master) -[![Dependency Status](https://gemnasium.com/heroku/heroku.svg)](https://gemnasium.com/heroku/heroku) Setup ----- From d68e70729409901815bc0a5a03b52ee961126293 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 6 Nov 2014 14:00:30 -0800 Subject: [PATCH 131/952] cleaned up README.md Added an icon Removed bit on development (anyone developing on it would need to know more than just that). Removed table describing various installation types since the toolbelt website will sniff the user agent (or provides the same information). Updated link to always go to homepage New dev center link (goes to the same place) --- README.md | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 12a823886..d4111f8ef 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -Heroku CLI +![](https://d4yt8xl9b7in.cloudfront.net/assets/home/logotype-heroku.png) Heroku CLI ========== The Heroku CLI is used to manage Heroku apps from the command line. -For more about Heroku see . +For more about Heroku see -To get started see +To get started see [![Build Status](https://travis-ci.org/heroku/heroku.svg?branch=master)](https://travis-ci.org/heroku/heroku) [![Coverage Status](https://img.shields.io/coveralls/heroku/heroku.svg)](https://coveralls.io/r/heroku/heroku?branch=master) @@ -13,28 +13,7 @@ To get started see Setup ----- - - - - - - - - - - - - - - - - - - - - - -
    If you have...Install with...
    Mac OS XDownload OS X package
    WindowsDownload Windows .exe installer
    Ubuntu Linuxapt-get repository
    OtherTarball (add contents to your $PATH)
    +First, [Install the Heroku CLI with the Toolbelt](https://toolbelt.heroku.com). Once installed, you'll have access to the `heroku` command from your command shell. Log in using the email address and password you used when creating your Heroku account: @@ -54,13 +33,6 @@ API For additional information about the API see [Heroku API Quickstart](https://devcenter.heroku.com/articles/platform-api-quickstart) and [Heroku API Reference](https://devcenter.heroku.com/articles/platform-api-reference). -Development ------------ - -If you're working on the CLI and you can smoke-test your changes: - - $ bundle exec heroku - Meta ---- From a87da81245361f221e94329bab499081dc9e71fa Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 14:12:17 -0800 Subject: [PATCH 132/952] Added Toolbelt release tasks This is so we no longer have to use the toolbelt repo for CLI releases. Now that the code is consolidated, a single `rake release` will push out the tgz, zip, gem and deb packages. The pkg and exe builds are not as important since they auto-update. Also refactored release code rake tasks. --- .gitignore | 2 +- Rakefile | 180 +----------------- dist/deb.rake | 32 ---- dist/gem.rake | 16 -- dist/manifest.rake | 11 -- dist/pkg.rake | 56 ------ dist/resources/deb/heroku-release-key.txt | 30 --- dist/resources/pkg/Distribution.erb | 15 -- dist/resources/pkg/PackageInfo.erb | 6 - dist/resources/pkg/postinstall | 45 ----- dist/rpm.rake | 35 ---- dist/tgz.rake | 26 --- dist/zip.rake | 40 ---- lib/heroku/distribution.rb | 9 - .../deb/heroku-toolbelt/apt-ftparchive.conf | 4 + resources/deb/heroku-toolbelt/control | 9 + .../deb => resources/deb/heroku}/control | 0 .../deb => resources/deb/heroku}/heroku | 0 .../deb => resources/deb/heroku}/postinst | 0 {dist/resources => resources}/tgz/heroku | 0 tasks/deb.rake | 72 +++++++ tasks/gem.rake | 12 ++ tasks/git.rake | 7 + tasks/helpers/file.rb | 59 ++++++ tasks/helpers/s3.rb | 31 +++ tasks/manifest.rake | 17 ++ .../pkg => tasks/resources/tgz}/heroku | 2 +- tasks/rspec.rake | 5 + tasks/tgz.rake | 27 +++ tasks/zip.rake | 43 +++++ 30 files changed, 296 insertions(+), 495 deletions(-) delete mode 100644 dist/deb.rake delete mode 100644 dist/gem.rake delete mode 100644 dist/manifest.rake delete mode 100644 dist/pkg.rake delete mode 100644 dist/resources/deb/heroku-release-key.txt delete mode 100644 dist/resources/pkg/Distribution.erb delete mode 100644 dist/resources/pkg/PackageInfo.erb delete mode 100755 dist/resources/pkg/postinstall delete mode 100644 dist/rpm.rake delete mode 100644 dist/tgz.rake delete mode 100644 dist/zip.rake delete mode 100644 lib/heroku/distribution.rb create mode 100644 resources/deb/heroku-toolbelt/apt-ftparchive.conf create mode 100644 resources/deb/heroku-toolbelt/control rename {dist/resources/deb => resources/deb/heroku}/control (100%) rename {dist/resources/deb => resources/deb/heroku}/heroku (100%) rename {dist/resources/deb => resources/deb/heroku}/postinst (100%) rename {dist/resources => resources}/tgz/heroku (100%) create mode 100644 tasks/deb.rake create mode 100644 tasks/gem.rake create mode 100644 tasks/git.rake create mode 100644 tasks/helpers/file.rb create mode 100644 tasks/helpers/s3.rb create mode 100644 tasks/manifest.rake rename {dist/resources/pkg => tasks/resources/tgz}/heroku (94%) create mode 100644 tasks/rspec.rake create mode 100644 tasks/tgz.rake create mode 100644 tasks/zip.rake diff --git a/.gitignore b/.gitignore index 4fc5d3fa0..a25a89285 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /.bundle /.rvmrc /coverage -/pkg +/dist /rdoc /tags /vendor diff --git a/Rakefile b/Rakefile index 29ca58b07..d347e50b9 100644 --- a/Rakefile +++ b/Rakefile @@ -1,183 +1,19 @@ -require "rubygems" +require "bundler/setup" PROJECT_ROOT = File.expand_path("..", __FILE__) $:.unshift "#{PROJECT_ROOT}/lib" - -require "heroku/version" -begin - require "rspec/core/rake_task" - - desc "Run all specs" - RSpec::Core::RakeTask.new(:spec) do |t| - t.verbose = true - end -rescue LoadError - # The test gem group fails to install on the platform for some reason -end - -task :default => :spec - -## dist - -require "erb" -require "fileutils" -require "tmpdir" - -def assemble(source, target, perms=0644) - FileUtils.mkdir_p(File.dirname(target)) - File.open(target, "w") do |f| - f.puts ERB.new(File.read(source)).result(binding) - end - File.chmod(perms, target) -end - -def assemble_distribution(target_dir=Dir.pwd) - distribution_files.each do |source| - target = source.gsub(/^#{project_root}/, target_dir) - FileUtils.mkdir_p(File.dirname(target)) - FileUtils.cp(source, target) - end -end - -GEM_BLACKLIST = %w( bundler heroku ) - -def assemble_gems(target_dir=Dir.pwd) - lines = %x{ bundle show }.strip.split("\n") - raise "error running bundler" unless $?.success? - - %x{ env BUNDLE_WITHOUT="development:test" bundle show }.split("\n").each do |line| - if line =~ /^ \* (.*?) \((.*?)\)/ - next if GEM_BLACKLIST.include?($1) - puts "vendoring: #{$1}-#{$2}" - gem_dir = %x{ bundle show #{$1} }.strip - FileUtils.mkdir_p "#{target_dir}/vendor/gems" - %x{ cp -R "#{gem_dir}" "#{target_dir}/vendor/gems" } - end - end.compact -end - -def beta? - Heroku::VERSION.to_s =~ /pre/ -end - -def clean(file) - rm file if File.exists?(file) -end - -def distribution_files(type=nil) - require "heroku/distribution" - base_files = Heroku::Distribution.files - type_files = type ? - Dir[File.expand_path("../dist/resources/#{type}/**/*", __FILE__)] : - [] - #base_files.concat(type_files) - base_files -end - -def mkchdir(dir) - FileUtils.mkdir_p(dir) - Dir.chdir(dir) do |dir| - yield(File.expand_path(dir)) - end -end - -def pkg(filename) - FileUtils.mkdir_p("pkg") - File.expand_path("../pkg/#{filename}", __FILE__) -end - -def project_root - File.dirname(__FILE__) -end - -def resource(name) - File.expand_path("../dist/resources/#{name}", __FILE__) -end - -def s3_connect - return if @s3_connected - - require "aws/s3" - - unless ENV["HEROKU_RELEASE_ACCESS"] && ENV["HEROKU_RELEASE_SECRET"] - puts "please set HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET in your environment" - exit 1 - end - - AWS::S3::Base.establish_connection!( - :access_key_id => ENV["HEROKU_RELEASE_ACCESS"], - :secret_access_key => ENV["HEROKU_RELEASE_SECRET"] - ) - - @s3_connected = true -end - -def store(package_file, filename, bucket="assets.heroku.com") - s3_connect - puts "storing: #{filename}" - AWS::S3::S3Object.store(filename, File.open(package_file), bucket, :access => :public_read) -end - -def tempdir - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - yield(dir) - end - end -end +require "heroku" def version - require "heroku/version" Heroku::VERSION end -Dir[File.expand_path("../dist/**/*.rake", __FILE__)].each do |rake| - import rake -end - -desc("Create a new changelog article") -task "changelog" do - changelog = <<-CHANGELOG -Heroku CLI v#{version} released with - -A new version of the Heroku CLI is available with +Dir.glob('tasks/helpers/*.rb').each { |r| import r } +Dir.glob('tasks/*.rake').each { |r| import r } -See the [CLI changelog](https://github.com/heroku/heroku/blob/master/CHANGELOG) for details and update by using \\`heroku update\\`. -CHANGELOG - - `echo "#{changelog}" | pbcopy` - - `open http://devcenter.heroku.com/admin/changelog_items/new` -end - -desc("Release the latest version") -task "release" => ["tgz:release", "zip:release", "manifest:update"] do - puts("Released v#{version}") +desc "release v#{version}" +task "release" => ["tgz:release", "zip:release", "manifest:update", "gem:release", "git:tag"] do + puts("released v#{version}") end -desc("Display statistics") -task "stats" do - require "heroku/command" - Dir[File.join(File.dirname(__FILE__), 'lib', 'heroku', 'command', '*.rb')].each do |file| - require(file) - end - commands, namespaces = Hash.new {|hash, key| hash[key] = 0}, [] - Heroku::Command.commands.keys.each do |key| - data = key.split(':') - unless data.first == data.last - commands[data.last] += 1 - end - namespaces |= [data.first] - end - puts "#{namespaces.length} Namespaces:" - puts "#{namespaces.join(', ')}" - puts - puts "#{commands.keys.length} Commands:" - max = commands.values.max - max.downto(0).each do |count| - keys = commands.keys.select {|key| commands[key] == count} - unless keys.empty? - puts("#{count}x #{keys.join(', ')}") - end - end -end +task :default => :spec diff --git a/dist/deb.rake b/dist/deb.rake deleted file mode 100644 index ad9a13571..000000000 --- a/dist/deb.rake +++ /dev/null @@ -1,32 +0,0 @@ -file pkg("/apt-#{version}/heroku-#{version}.deb") => distribution_files("deb") do |t| - mkchdir(File.dirname(t.name)) do - mkchdir("usr/local/heroku") do - assemble_distribution - assemble_gems - assemble resource("deb/heroku"), "bin/heroku", 0755 - end - - assemble resource("deb/control"), "control" - assemble resource("deb/postinst"), "postinst" - - sh "tar czvf data.tar.gz usr/local/heroku --owner=root --group=root" - sh "tar czvf control.tar.gz control postinst" - - File.open("debian-binary", "w") do |f| - f.puts "2.0" - end - - deb = File.basename(t.name) - - sh "ar -r #{t.name} debian-binary control.tar.gz data.tar.gz" - end -end - -desc "Build a .deb package" -task "deb:build" => pkg("/apt-#{version}/heroku-#{version}.deb") - -desc "Remove build artifacts for .deb" -task "deb:clean" do - clean pkg("heroku-#{version}.deb") - FileUtils.rm_rf("pkg/apt-#{version}") if Dir.exists?("pkg/apt-#{version}") -end diff --git a/dist/gem.rake b/dist/gem.rake deleted file mode 100644 index 293b4d249..000000000 --- a/dist/gem.rake +++ /dev/null @@ -1,16 +0,0 @@ -file pkg("heroku-#{version}.gem") => distribution_files("gem") do |t| - sh "gem build heroku.gemspec" - sh "mv heroku-#{version}.gem #{t.name}" -end - -task "gem:build" => pkg("heroku-#{version}.gem") - -task "gem:clean" do - clean pkg("heroku-#{version}.gem") -end - -task "gem:release" => "gem:build" do |t| - sh "gem push #{pkg("heroku-#{version}.gem")}" - sh "git tag v#{version}" - sh "git push origin master --tags" -end diff --git a/dist/manifest.rake b/dist/manifest.rake deleted file mode 100644 index adaf03b04..000000000 --- a/dist/manifest.rake +++ /dev/null @@ -1,11 +0,0 @@ -task "manifest:update" do - abort "Manifest should never contain betas." if beta? - - tempdir do |dir| - File.open("VERSION", "w") do |file| - file.puts version - end - puts "Current version: #{version}" - store "#{dir}/VERSION", "heroku-client/VERSION" - end -end diff --git a/dist/pkg.rake b/dist/pkg.rake deleted file mode 100644 index 01142f7a3..000000000 --- a/dist/pkg.rake +++ /dev/null @@ -1,56 +0,0 @@ -require "erb" - -file pkg("heroku-#{version}.pkg") => distribution_files("pkg") do |t| - tempdir do |dir| - mkchdir("heroku-client") do - assemble_distribution - assemble_gems - assemble resource("pkg/heroku"), "bin/heroku", 0755 - end - - kbytes = %x{ du -ks heroku-client | cut -f 1 } - num_files = %x{ find heroku-client | wc -l } - - mkdir_p "pkg" - mkdir_p "pkg/Resources" - mkdir_p "pkg/heroku-client.pkg" - - dist = File.read(resource("pkg/Distribution.erb")) - dist = ERB.new(dist).result(binding) - File.open("pkg/Distribution", "w") { |f| f.puts dist } - - dist = File.read(resource("pkg/PackageInfo.erb")) - dist = ERB.new(dist).result(binding) - File.open("pkg/heroku-client.pkg/PackageInfo", "w") { |f| f.puts dist } - - mkdir_p "pkg/heroku-client.pkg/Scripts" - cp resource("pkg/postinstall"), "pkg/heroku-client.pkg/Scripts/postinstall" - chmod 0755, "pkg/heroku-client.pkg/Scripts/postinstall" - - sh %{ mkbom -s heroku-client pkg/heroku-client.pkg/Bom } - - Dir.chdir("heroku-client") do - sh %{ pax -wz -x cpio . > ../pkg/heroku-client.pkg/Payload } - end - - sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o ruby.pkg } - sh %{ pkgutil --expand ruby.pkg ruby } - mv "ruby/ruby-1.9.3-p194.pkg", "pkg/ruby.pkg" - - sh %{ pkgutil --flatten pkg heroku-#{version}.pkg } - - cp_r "heroku-#{version}.pkg", t.name - end -end - -desc "build pkg" -task "pkg:build" => pkg("heroku-#{version}.pkg") - -desc "clean pkg" -task "pkg:clean" do - clean pkg("heroku-#{version}.pkg") -end - -task "pkg:release" do - raise "pkg:release moved to toolbelt repo" -end diff --git a/dist/resources/deb/heroku-release-key.txt b/dist/resources/deb/heroku-release-key.txt deleted file mode 100644 index e22a1c2a8..000000000 --- a/dist/resources/deb/heroku-release-key.txt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.11 (Darwin) - -mQENBE5SfAEBCADLp056ZgfdtAMXLWpEuL9zY+dIHIY5qLQcDmUivjHLVE4l3Bi3 -Mn570K0W9rfk7fHBPEO2XJEDdjk8Bg6mWTAeGjdfZgZaL+qO9NjqQ5QmVR+vgp7s -yxJYlfY+JYTZvl/JiDWGhuPHSPggXILCMf3SpqWMHGPqe/3RAK+CHCNv/94uaoS4 -vi4HQT+k4sRceiM8WqkSRYSoc7rzdDejZn+InCYFfR56VeSFF4G4I6neZs/q5T9d -Ty2i5d0gZLaX/Iqc+3Dy0vDKClc0HUQJ6ajDPuUqKLHFUpqyuwfJij60+C3GMi8K -ckRPti31EPFVzq3GPHU+GqA+e9j84WHr4uJ5ABEBAAG0L0hlcm9rdSBSZWxlYXNl -IEVuZ2luZWVyaW5nIDxyZWxlYXNlQGhlcm9rdS5jb20+iQE4BBMBAgAiBQJOUnwB -AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRDJJ+vgDxsFIChECAC9h4Ay -Nx4AQFu85cjR9rijyBflPeVqi7Xhzd7IvLg2+kZSexlb2oidj7iVSMy+vy5tG9g9 -8Az/JqMCVjcZ7ltn60OGU8gIYpJqt6VmH3vfJBxXu/Sm9tym3UCYGVvMAN5Oq6yB -HlQkQ8F3p0cW69PmF+fibkgo9RE0EYlBIt2rUHNilTGFS6vXGr5reFFp3/rRHq3k -bixnUwFSqNujJgnBKDPwtSYKc4pMpnhuv88xEpLH7vU8NLXQZMitKQguV8XEmcsu -43LXlsx5uVr239/XNW+h412gIHFDSzB/YuLWlVUXMfquC96z/wxMqWWZyskDNgr0 -WDdMgzK6CUfXSqQhuQENBE5SfAEBCADbnGKcXpdVauQpINQLtRnrT0BJIrIo1Yxv -LQRb3G7RU+Eq6aHXwk9fSKa6nEv9RsmqiW874yODnr0d/DTUWMHT+jRvPHm1wlbE -pGR1aPSo7GgkSUdaT6CVBN3JWZ2kVJGqohNoJMYbfVaWd/kpa/LiMFWzS8LfWT2K -xiO2vIh4qBfeRCGR7s8rADCHuHJ0eibADrgqcRfdPrChB1JiYLeTdV4yRmSzJ7TM -zWX7OVpGfIFLbCw9NeN65pI9ePs2mSPM7DYkhhKSXWMwJNXFzn1blOGiwAwKb48P -a/QpE6TG3PQzbYyTTP0Td1XgKAHcprvbc89a/nAk3a+PJQ/MqvDzABEBAAGJAR8E -GAECAAkFAk5SfAECGwwACgkQySfr4A8bBSD4mAgAnCT5WRiDl0259Px9Z9J9Wk8Z -SxugDct2Yhzca4aw1Ou4cfaIFCDXzFlBzSJfqk0HoVhp9r2gzEPUCKnSjRDyxaMo -wZCUtqigBua+z4NB4AWgeOl/2S06I2ki1K7pfl4piYcHtEThHamnhVPJ2Hi6HsHq -mUU+8SxleHE4GCXmKkuvxelUq9jrhHikIkm1RoqFOPb9zV3WRy4YzVHQSYfHmfk0 -9kXlM/CS0sfNv2UKCX+5e6eFIZv0rdtpp6VEh0tsFmsIClY6Z9MX7bgp8MnUJpyk -OeIzOzQgkb4aeT0Whl+EPcTeDZfqIhVBoNXupUanmWNppFcMngxfqG2NGi1vvQ== -=aUAq ------END PGP PUBLIC KEY BLOCK----- diff --git a/dist/resources/pkg/Distribution.erb b/dist/resources/pkg/Distribution.erb deleted file mode 100644 index 795215b31..000000000 --- a/dist/resources/pkg/Distribution.erb +++ /dev/null @@ -1,15 +0,0 @@ - - - Heroku Client - - - - - - - - - - #heroku-client.pkg - #ruby.pkg - diff --git a/dist/resources/pkg/PackageInfo.erb b/dist/resources/pkg/PackageInfo.erb deleted file mode 100644 index 5a0da8998..000000000 --- a/dist/resources/pkg/PackageInfo.erb +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/dist/resources/pkg/postinstall b/dist/resources/pkg/postinstall deleted file mode 100755 index f9cb2a062..000000000 --- a/dist/resources/pkg/postinstall +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/sh - -usershell=$(dscl localhost -read /Local/Default/Users/$USER shell | sed -e 's/[^ ]* //') - -startup_files() { - case $(basename $usershell) in - zsh) - echo ".zlogin .zshrc .zprofile .zshenv" - ;; - bash) - echo ".bashrc .bash_profile .bash_login .profile" - ;; - *) - echo ".bash_profile .zshrc .profile" - ;; - esac -} - -install_path() { - for file in $(startup_files); do - [ -f $HOME/$file ] || continue - (grep "Added by the Heroku" $HOME/$file >/dev/null) && break - - cat <>$HOME/$file - -### Added by the Heroku Toolbelt -export PATH="/usr/local/heroku/bin:\$PATH" -MESSAGE - - # done after we add to one file - break - done -} - -# if the toolbelt is not returned by `which`, let's add to the PATH -case $(which heroku) in - /usr/bin/heroku|/usr/local/heroku/bin/heroku) - ;; - *) - install_path - ;; -esac - -# symlink binary to /usr/bin/heroku -ln -sf /usr/local/heroku/bin/heroku /usr/bin/heroku diff --git a/dist/rpm.rake b/dist/rpm.rake deleted file mode 100644 index 96b12edaf..000000000 --- a/dist/rpm.rake +++ /dev/null @@ -1,35 +0,0 @@ -# TODO -# * signing -# * yum repository for updates -# * foreman - -file pkg("/yum-#{version}/heroku-#{version}.rpm") => "deb:build" do |t| - mkchdir(File.dirname(t.name)) do - deb = pkg("/apt-#{version}/heroku-#{version}.deb") - sh "alien --keep-version --scripts --generate --to-rpm #{deb}" - - spec = "heroku-#{version}/heroku-#{version}-1.spec" - spec_contents = File.read(spec) - File.open(spec, "w") do |f| - # Add ruby requirement, remove benchmark file with ugly filename - f.puts spec_contents.sub(/\n\n/m, "\nRequires: ruby\nBuildArch: noarch\n\n"). - sub(/^.+has_key-vs-hash\[key\].+$/, ""). - sub(/^License: .*/, "License: MIT\nURL: http://heroku.com\n"). - sub(/^%description/, "%description\nClient library and CLI to deploy apps on Heroku.") - end - sh "sed -i s/ruby1.9.1/ruby/ heroku-#{version}/usr/local/heroku/bin/heroku" - - chdir("heroku-#{version}") do - sh "rpmbuild --buildroot $PWD -bb heroku-#{version}-1.spec" - end - end -end - -desc "Build an .rpm package" -task "rpm:build" => pkg("/yum-#{version}/heroku-#{version}.rpm") - -desc "Remove build artifacts for .rpm" -task "rpm:clean" do - clean pkg("heroku-#{version}.rpm") - FileUtils.rm_rf("pkg/yum-#{version}") if Dir.exists?("pkg/yum-#{version}") -end diff --git a/dist/tgz.rake b/dist/tgz.rake deleted file mode 100644 index 315c192e1..000000000 --- a/dist/tgz.rake +++ /dev/null @@ -1,26 +0,0 @@ -file pkg("heroku-#{version}.tgz") => distribution_files("tgz") do |t| - tempdir do |dir| - mkchdir("heroku-client") do - assemble_distribution - assemble_gems - assemble resource("tgz/heroku"), "bin/heroku", 0755 - end - - sh "chmod -R go+r heroku-client" - sh "sudo chown -R 0:0 heroku-client" - sh "tar czf #{t.name} heroku-client" - sh "sudo chown -R $(whoami) heroku-client" - end -end - -task "tgz:build" => pkg("heroku-#{version}.tgz") - -task "tgz:clean" do - clean pkg("heroku-#{version}.tgz") -end - -task "tgz:release" => "tgz:build" do |t| - store pkg("heroku-#{version}.tgz"), "heroku-client/heroku-client-#{version}.tgz" - store pkg("heroku-#{version}.tgz"), "heroku-client/heroku-client-beta.tgz" if beta? - store pkg("heroku-#{version}.tgz"), "heroku-client/heroku-client.tgz" unless beta? -end diff --git a/dist/zip.rake b/dist/zip.rake deleted file mode 100644 index bc3a98af2..000000000 --- a/dist/zip.rake +++ /dev/null @@ -1,40 +0,0 @@ -require "zip/zip" - -file pkg("heroku-#{version}.zip") => distribution_files("zip") do |t| - tempdir do |dir| - mkchdir("heroku-client") do - assemble_distribution - assemble_gems - Zip::ZipFile.open(t.name, Zip::ZipFile::CREATE) do |zip| - Dir["**/*"].each do |file| - zip.add(file, file) { true } - end - end - end - end -end - -file pkg("heroku-#{version}.zip.sha256") => pkg("heroku-#{version}.zip") do |t| - File.open(t.name, "w") do |file| - file.puts Digest::SHA256.file(t.prerequisites.first).hexdigest - end -end - -task "zip:build" => pkg("heroku-#{version}.zip") -task "zip:sign" => pkg("heroku-#{version}.zip.sha256") - -def zip_signature - File.read(pkg("heroku-#{version}.zip.sha256")).chomp -end - -task "zip:clean" do - clean pkg("heroku-#{version}.zip") -end - -task "zip:release" => %w( zip:build zip:sign ) do |t| - store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client-#{version}.zip" - store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client-beta.zip" if beta? - store pkg("heroku-#{version}.zip"), "heroku-client/heroku-client.zip" unless beta? - - sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" unless beta? -end diff --git a/lib/heroku/distribution.rb b/lib/heroku/distribution.rb deleted file mode 100644 index 109ab188e..000000000 --- a/lib/heroku/distribution.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Heroku - module Distribution - def self.files - Dir[File.expand_path("../../../{bin,data,lib}/**/*", __FILE__)].select do |file| - File.file?(file) - end - end - end -end diff --git a/resources/deb/heroku-toolbelt/apt-ftparchive.conf b/resources/deb/heroku-toolbelt/apt-ftparchive.conf new file mode 100644 index 000000000..f8b6264a7 --- /dev/null +++ b/resources/deb/heroku-toolbelt/apt-ftparchive.conf @@ -0,0 +1,4 @@ +APT::FTPArchive::Release { + Origin "Heroku, Inc."; + Suite "stable"; +} diff --git a/resources/deb/heroku-toolbelt/control b/resources/deb/heroku-toolbelt/control new file mode 100644 index 000000000..726cef4ae --- /dev/null +++ b/resources/deb/heroku-toolbelt/control @@ -0,0 +1,9 @@ +Package: heroku-toolbelt +Version: <%= version %> +Section: main +Priority: standard +Architecture: all +Depends: git-core, foreman, heroku (= <%= version %>) +Installed-Size: +Maintainer: Heroku +Description: A metapackage for working with the Heroku platform. diff --git a/dist/resources/deb/control b/resources/deb/heroku/control similarity index 100% rename from dist/resources/deb/control rename to resources/deb/heroku/control diff --git a/dist/resources/deb/heroku b/resources/deb/heroku/heroku similarity index 100% rename from dist/resources/deb/heroku rename to resources/deb/heroku/heroku diff --git a/dist/resources/deb/postinst b/resources/deb/heroku/postinst similarity index 100% rename from dist/resources/deb/postinst rename to resources/deb/heroku/postinst diff --git a/dist/resources/tgz/heroku b/resources/tgz/heroku similarity index 100% rename from dist/resources/tgz/heroku rename to resources/tgz/heroku diff --git a/tasks/deb.rake b/tasks/deb.rake new file mode 100644 index 000000000..900e94dba --- /dev/null +++ b/tasks/deb.rake @@ -0,0 +1,72 @@ +FOREMAN_VERSION = "0.75.0" + +namespace :deb do + desc "build deb" + task :build => dist("heroku-toolbelt-#{version}.apt") + + desc "release deb" + task :release => :build do |t| + s3_store_dir dist("heroku-toolbelt-#{version}.apt"), "apt", "heroku-toolbelt" + end + + file dist("heroku-toolbelt-#{version}.apt") => [ dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-toolbelt-#{version}.deb") ] do |t| + abort "Don't publish .debs of pre-releases!" if version =~ /[a-zA-Z]$/ + + cd t.name do |dir| + touch "Sources" + + sh "apt-ftparchive packages . > Packages" + sh "gzip -c Packages > Packages.gz" + sh "apt-ftparchive -c #{resource("deb/heroku-toolbelt/apt-ftparchive.conf")} release . > Release" + sh "gpg -abs -u 0F1B0520 -o Release.gpg Release" + end + end + + + file dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb") do |t| + mkdir_p File.dirname(t.name) + unless File.exist? "dist/foreman" + sh "git clone git@github.com:ddollar/foreman.git dist/foreman" + end + cd "dist/foreman" do + sh "git checkout v#{FOREMAN_VERSION}" + rm_rf ".bundle" + rm_rf "apt-#{FOREMAN_VERSION}" + Bundler.with_clean_env do + sh "unset GEM_HOME RUBYOPT; bundle install --path vendor/bundle" or abort + sh "unset GEM_HOME RUBYOPT; bundle exec rake deb:build" or abort + end + mv "pkg/apt-#{FOREMAN_VERSION}/foreman-#{FOREMAN_VERSION}.deb", t.name + end + end + + file dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb") => distribution_files("deb") do |t| + tempdir do + mkdir_p "usr/local/heroku" + cd "usr/local/heroku" do + assemble_distribution + assemble_gems + assemble resource("deb/heroku/heroku"), "bin/heroku", 0755 + end + + assemble resource("deb/heroku/control"), "control" + assemble resource("deb/heroku/postinst"), "postinst" + + sh "tar czf data.tar.gz usr/local/heroku --owner=root --group=root" + sh "tar czf control.tar.gz control postinst" + + File.open("debian-binary", "w") do |f| + f.puts "2.0" + end + + sh "ar -r #{t.name} debian-binary control.tar.gz data.tar.gz" + end + end + + file dist("heroku-toolbelt-#{version}.apt/heroku-toolbelt-#{version}.deb") do |t| + tempdir do |dir| + assemble resource("deb/heroku-toolbelt/control"), "DEBIAN/control" + sh "dpkg-deb --build . #{t.name}" + end + end +end diff --git a/tasks/gem.rake b/tasks/gem.rake new file mode 100644 index 000000000..7de28aa6c --- /dev/null +++ b/tasks/gem.rake @@ -0,0 +1,12 @@ +namespace :gem do + desc "build gem" + task :build do + sh "gem build heroku.gemspec" + mv "heroku-#{version}.gem", dist("heroku-#{version}.gem") + end + + desc "release gem" + task :release => :build do + sh "gem push #{dist("heroku-#{version}.gem")}" + end +end diff --git a/tasks/git.rake b/tasks/git.rake new file mode 100644 index 000000000..317f24eb5 --- /dev/null +++ b/tasks/git.rake @@ -0,0 +1,7 @@ +namespace :git do + desc "tags the repo at the current version and pushes it to github" + task :tag do + sh "git tag v#{version}" + sh "git push origin v#{version}" + end +end diff --git a/tasks/helpers/file.rb b/tasks/helpers/file.rb new file mode 100644 index 000000000..eff5e08fe --- /dev/null +++ b/tasks/helpers/file.rb @@ -0,0 +1,59 @@ +require "erb" +require "fileutils" +require "tmpdir" + +GEM_BLACKLIST = %w( bundler heroku ) + +def assemble(source, target, perms=0644) + FileUtils.mkdir_p(File.dirname(target)) + File.open(target, "w") do |f| + f.puts ERB.new(File.read(source)).result(binding) + end + File.chmod(perms, target) +end + +def assemble_distribution(target_dir=Dir.pwd) + distribution_files.each do |source| + target = source.gsub(/^#{PROJECT_ROOT}/, target_dir) + FileUtils.mkdir_p(File.dirname(target)) + FileUtils.cp(source, target) + end +end + +def assemble_gems(target_dir=Dir.pwd) + %x{ env BUNDLE_WITHOUT="development:test" bundle show }.split("\n").each do |line| + if line =~ /^ \* (.*?) \((.*?)\)/ + next if GEM_BLACKLIST.include?($1) + gem_dir = %x{ bundle show #{$1} }.strip + FileUtils.mkdir_p "#{target_dir}/vendor/gems" + %x{ cp -R "#{gem_dir}" "#{target_dir}/vendor/gems" } + end + end.compact +end + +def beta? + Heroku::VERSION.to_s =~ /pre/ +end + +def distribution_files(type=nil) + Dir[File.expand_path("{bin,data,lib}/**/*", PROJECT_ROOT)].select do |file| + File.file?(file) + end +end + +def dist(filename) + FileUtils.mkdir_p("dist") + File.expand_path("dist/#{filename}", PROJECT_ROOT) +end + +def resource(name) + File.expand_path("resources/#{name}", PROJECT_ROOT) +end + +def tempdir + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + yield(dir) + end + end +end diff --git a/tasks/helpers/s3.rb b/tasks/helpers/s3.rb new file mode 100644 index 000000000..c50a4d34a --- /dev/null +++ b/tasks/helpers/s3.rb @@ -0,0 +1,31 @@ +def s3_connect + return if @s3_connected + + require "aws/s3" + + unless ENV["HEROKU_RELEASE_ACCESS"] && ENV["HEROKU_RELEASE_SECRET"] + puts "please set HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET in your environment" + exit 1 + end + + AWS::S3::Base.establish_connection!( + :access_key_id => ENV["HEROKU_RELEASE_ACCESS"], + :secret_access_key => ENV["HEROKU_RELEASE_SECRET"] + ) + + @s3_connected = true +end + +def s3_store(package_file, filename, bucket="assets.heroku.com") + s3_connect + puts "storing: #{filename}" + AWS::S3::S3Object.store(filename, File.open(package_file), bucket, :access => :public_read) +end + +def s3_store_dir(from, to, bucket="assets.heroku.com") + Dir.glob(File.Join(from, "**", "*")).each do |file| + next if File.directory?(file) + remote = file.gsub(from, to) + s3_store file, remote, bucket + end +end diff --git a/tasks/manifest.rake b/tasks/manifest.rake new file mode 100644 index 000000000..16ca3543b --- /dev/null +++ b/tasks/manifest.rake @@ -0,0 +1,17 @@ +namespace :manifest do + desc "puts VERSION file into s3" + task :update do + if beta? + $stderr.puts "skipping manifest:update since this is a beta release" + next + end + + tempdir do |dir| + File.open("VERSION", "w") do |file| + file.puts version + end + puts "Current version: #{version}" + s3_store "#{dir}/VERSION", "heroku-client/VERSION" + end + end +end diff --git a/dist/resources/pkg/heroku b/tasks/resources/tgz/heroku similarity index 94% rename from dist/resources/pkg/heroku rename to tasks/resources/tgz/heroku index 710d628d9..8b09b7e66 100644 --- a/dist/resources/pkg/heroku +++ b/tasks/resources/tgz/heroku @@ -1,4 +1,4 @@ -#!/usr/local/heroku/ruby/bin/ruby +#!/usr/bin/env ruby # encoding: UTF-8 # resolve bin path, ignoring symlinks diff --git a/tasks/rspec.rake b/tasks/rspec.rake new file mode 100644 index 000000000..63a721d19 --- /dev/null +++ b/tasks/rspec.rake @@ -0,0 +1,5 @@ +begin + require 'rspec/core/rake_task' + RSpec::Core::RakeTask.new(:spec) +rescue LoadError +end diff --git a/tasks/tgz.rake b/tasks/tgz.rake new file mode 100644 index 000000000..ecac65d4d --- /dev/null +++ b/tasks/tgz.rake @@ -0,0 +1,27 @@ +namespace :tgz do + desc "build tgz" + task :build => dist("heroku-#{version}.tgz") + + desc "release tgz" + task :release => :build do |t| + s3_store dist("heroku-#{version}.tgz"), "heroku-client/heroku-client-#{version}.tgz" + s3_store dist("heroku-#{version}.tgz"), "heroku-client/heroku-client-beta.tgz" if beta? + s3_store dist("heroku-#{version}.tgz"), "heroku-client/heroku-client.tgz" unless beta? + end + + file dist("heroku-#{version}.tgz") => distribution_files("tgz") do |t| + tempdir do |dir| + mkdir "heroku-client" + cd "heroku-client" do + assemble_distribution + assemble_gems + assemble resource("tgz/heroku"), "bin/heroku", 0755 + end + + sh "chmod -R go+r heroku-client" + sh "sudo chown -R 0:0 heroku-client" + sh "tar czf #{t.name} heroku-client" + sh "sudo chown -R $(whoami) heroku-client" + end + end +end diff --git a/tasks/zip.rake b/tasks/zip.rake new file mode 100644 index 000000000..194abc78d --- /dev/null +++ b/tasks/zip.rake @@ -0,0 +1,43 @@ +require "zip/zip" + +namespace :zip do + desc "build zip" + task :build => dist("heroku-#{version}.zip") + + desc "sign zip" + task :sign => dist("heroku-#{version}.zip.sha256") + + desc "release zip" + task :release => [:build, :sign] do |t| + s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client-#{version}.zip" + s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client-beta.zip" if beta? + s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client.zip" unless beta? + + sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" unless beta? + end + + file dist("heroku-#{version}.zip") => distribution_files("zip") do |t| + tempdir do |dir| + mkdir "heroku-client" + cd "heroku-client" do + assemble_distribution + assemble_gems + Zip::ZipFile.open(t.name, Zip::ZipFile::CREATE) do |zip| + Dir["**/*"].each do |file| + zip.add(file, file) { true } + end + end + end + end + end + + file dist("heroku-#{version}.zip.sha256") => dist("heroku-#{version}.zip") do |t| + File.open(t.name, "w") do |file| + file.puts Digest::SHA256.file(t.prerequisites.first).hexdigest + end + end + + def zip_signature + File.read(dist("heroku-#{version}.zip.sha256")).chomp + end +end From 9eecdc38ae08e5c21a2a909de0c0231ef9e260c4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 17:33:46 -0800 Subject: [PATCH 133/952] added deb release to release rake task --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index d347e50b9..a961a9338 100644 --- a/Rakefile +++ b/Rakefile @@ -12,7 +12,7 @@ Dir.glob('tasks/helpers/*.rb').each { |r| import r } Dir.glob('tasks/*.rake').each { |r| import r } desc "release v#{version}" -task "release" => ["tgz:release", "zip:release", "manifest:update", "gem:release", "git:tag"] do +task "release" => ["tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do puts("released v#{version}") end From 50e7015b756cd67afe5aef43fb5ffa14266c04cf Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 7 Nov 2014 10:32:23 -0800 Subject: [PATCH 134/952] update the stack example out --- lib/heroku/command/stack.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/stack.rb b/lib/heroku/command/stack.rb index 3415eb5cc..009177654 100644 --- a/lib/heroku/command/stack.rb +++ b/lib/heroku/command/stack.rb @@ -13,9 +13,8 @@ class Stack < Base # # $ heroku stack # === example Available Stacks - # bamboo-mri-1.9.2 - # bamboo-ree-1.8.7 - # * cedar + # cedar + # * cedar-14 # def index validate_arguments! From 4475a702a10e0f0cc1164edec1d9b98cc7908dcd Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 10 Nov 2014 13:13:07 -0800 Subject: [PATCH 135/952] v3.15.3 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f1aa6ee69..45587facd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.15.3 2014-11-10 +================= +Removed bamboo from stack command +Reverted 2fa input masking + 3.15.2 2014-11-04 ================= Mask 2fa inputs longer than 12 characters diff --git a/Gemfile.lock b/Gemfile.lock index 12f853733..6546d91e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.15.2) + heroku (3.15.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 153b6bef5..3c582118a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.15.2" + VERSION = "3.15.3" end From 6ae72fe104435815dbe7cac2af2de27421a28e0f Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Mon, 10 Nov 2014 14:01:00 -0800 Subject: [PATCH 136/952] updated year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index e47d0a1f6..ffaa7a305 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © Heroku 2008 - 2012 +Copyright © Heroku 2008 - 2014 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From 2dfb2aaad52b0dc561ed57ac7c5983deccd74b80 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 10 Nov 2014 14:08:36 -0800 Subject: [PATCH 137/952] fix(release): fixed bug with s3 uploading of folders --- tasks/helpers/s3.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/helpers/s3.rb b/tasks/helpers/s3.rb index c50a4d34a..0cb758869 100644 --- a/tasks/helpers/s3.rb +++ b/tasks/helpers/s3.rb @@ -23,7 +23,7 @@ def s3_store(package_file, filename, bucket="assets.heroku.com") end def s3_store_dir(from, to, bucket="assets.heroku.com") - Dir.glob(File.Join(from, "**", "*")).each do |file| + Dir.glob(File.join(from, "**", "*")).each do |file| next if File.directory?(file) remote = file.gsub(from, to) s3_store file, remote, bucket From 306525b5713f0c8390a28effa3953105f4621301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Clermont?= Date: Mon, 17 Nov 2014 19:07:38 +0100 Subject: [PATCH 138/952] fix(features): typos in features commands' usages --- lib/heroku/command/features.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/features.rb b/lib/heroku/command/features.rb index 3564a737f..eead80b74 100644 --- a/lib/heroku/command/features.rb +++ b/lib/heroku/command/features.rb @@ -45,7 +45,7 @@ def index # def info unless feature_name = shift_argument - error("Usage: heroku feature:info FEATURE\nMust specify FEATURE for info.") + error("Usage: heroku features:info FEATURE\nMust specify FEATURE for info.") end validate_arguments! @@ -68,7 +68,7 @@ def info # def disable feature_name = shift_argument - error "Usage: heroku feature:disable FEATURE\nMust specify FEATURE to disable." unless feature_name + error "Usage: heroku features:disable FEATURE\nMust specify FEATURE to disable." unless feature_name validate_arguments! feature = api.get_features(app).body.detect { |f| f["name"] == feature_name } @@ -88,7 +88,7 @@ def disable end end - # feature:enable FEATURE + # features:enable FEATURE # # enables an feature # From 636aa78651a91946bb3f73efa531160336557058 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 17 Nov 2014 16:34:03 -0800 Subject: [PATCH 139/952] unhide http-git-flag --- lib/heroku/command/git.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 466f42643..d84b14504 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -11,7 +11,7 @@ class Heroku::Command::Git < Heroku::Command::Base # clones a heroku app to your local machine at DIRECTORY (defaults to app name) # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --http-git # HIDDEN: Use HTTP git protocol + # --http-git # use HTTP git protocol # # #Examples: @@ -40,7 +40,7 @@ def clone # if OPTIONS are specified they will be passed to git remote add # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --http-git # HIDDEN: Use HTTP git protocol + # --http-git # use HTTP git protocol # #Examples: # From aaac9400a4463fc7028e723dbbdca58ee54681e7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 17 Nov 2014 16:57:09 -0800 Subject: [PATCH 140/952] updating tweaks do not block updates if starting updating more than 5 minutes ago extend time between update checks to 24 hours --- lib/heroku/cli.rb | 4 +++- lib/heroku/updater.rb | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index a886e3802..15f98b8ad 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,7 +1,9 @@ load('heroku/helpers.rb') # reload helpers after possible inject_loadpath load('heroku/updater.rb') # reload updater after possible inject_loadpath -if File.exist? Heroku::Updater.updating_lock_path +# exists and updated in the last 5 minutes +if File.exist?(Heroku::Updater.updating_lock_path) && + File.mtime(Heroku::Updater.updating_lock_path) > (Time.now - 5*60) $stderr.puts "Heroku Toolbelt is currently updating. Please wait a few seconds and try your command again." exit 1 end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index e940044d5..ea4afb237 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -164,9 +164,9 @@ def self.last_autoupdate_path end def self.background_update! - # if we've updated in the last 300 seconds, dont try again + # if we've updated in the last day, don't try again if File.exists?(last_autoupdate_path) - return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 300 + return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60*24 end log_path = File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate.log') FileUtils.mkdir_p File.dirname(log_path) From c4935110a90206925b01fd0cbd32bcecc17ce484 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 19 Nov 2014 15:46:34 -0800 Subject: [PATCH 141/952] added build rake task this is to be run before release so if there are any errors in building the CLI, it will bail out before partially releasing to some targets --- Rakefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index a961a9338..27f557238 100644 --- a/Rakefile +++ b/Rakefile @@ -12,8 +12,13 @@ Dir.glob('tasks/helpers/*.rb').each { |r| import r } Dir.glob('tasks/*.rake').each { |r| import r } desc "release v#{version}" -task "release" => ["tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do +task "release" => ["build", "tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do puts("released v#{version}") end +desc "build v#{version}" +task "build" => ["tgz:build", "zip:build", "deb:build", "gem:build"] do + puts("built v#{version}") +end + task :default => :spec From 991c10c9968bac450f8b1351be929d65da5c715c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 19 Nov 2014 16:33:03 -0800 Subject: [PATCH 142/952] added can_release step to release process this ensures that the CLI is not released if the released version is the current version (the dev didn't bump the version) --- Rakefile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index 27f557238..113dff08c 100644 --- a/Rakefile +++ b/Rakefile @@ -12,7 +12,7 @@ Dir.glob('tasks/helpers/*.rb').each { |r| import r } Dir.glob('tasks/*.rake').each { |r| import r } desc "release v#{version}" -task "release" => ["build", "tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do +task "release" => ["can_release", "build", "tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do puts("released v#{version}") end @@ -21,4 +21,12 @@ task "build" => ["tgz:build", "zip:build", "deb:build", "gem:build"] do puts("built v#{version}") end +desc "check to see if v#{version} is not already released" +task :can_release do + if `gem list ^heroku$ --remote` == "heroku (#{version})\n" + $stderr.puts "cannot release v#{version}" + exit(1) + end +end + task :default => :spec From b9a548d49ad4ffeee67117db79690557087dd865 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 19 Nov 2014 20:29:44 -0800 Subject: [PATCH 143/952] enhance release check by checking for release keys --- Rakefile | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 113dff08c..f53065d60 100644 --- a/Rakefile +++ b/Rakefile @@ -21,10 +21,14 @@ task "build" => ["tgz:build", "zip:build", "deb:build", "gem:build"] do puts("built v#{version}") end -desc "check to see if v#{version} is not already released" +desc "check to see if v#{version} is releaseable" task :can_release do + if ENV['HEROKU_RELEASE_ACCESS'].nil? || ENV['HEROKU_RELEASE_SECRET'].nil? + $stderr.puts "cannot release, #{version}, HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET must be set" + exit(1) + end if `gem list ^heroku$ --remote` == "heroku (#{version})\n" - $stderr.puts "cannot release v#{version}" + $stderr.puts "cannot release #{version}, v#{version} is already released" exit(1) end end From 1d73ca0367e74785720dfe9ed51183bc678b857b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 19 Nov 2014 20:33:40 -0800 Subject: [PATCH 144/952] use https git for foreman --- tasks/deb.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/deb.rake b/tasks/deb.rake index 900e94dba..f0e5cbe35 100644 --- a/tasks/deb.rake +++ b/tasks/deb.rake @@ -26,7 +26,7 @@ namespace :deb do file dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb") do |t| mkdir_p File.dirname(t.name) unless File.exist? "dist/foreman" - sh "git clone git@github.com:ddollar/foreman.git dist/foreman" + sh "git clone https://github.com/ddollar/foreman.git dist/foreman" end cd "dist/foreman" do sh "git checkout v#{FOREMAN_VERSION}" From b34150053320a4ed184eb520c0a51f0f34fee9d2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 19 Nov 2014 21:22:27 -0800 Subject: [PATCH 145/952] warn if git.heroku.com is not in .netrc and using https git commands --- lib/heroku/command/apps.rb | 1 + lib/heroku/command/git.rb | 1 + lib/heroku/helpers.rb | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index cbea1de1f..a6947386e 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -238,6 +238,7 @@ def create end git_url = if options[:http_git] + warn_if_netrc_does_not_have_https_git "https://#{Heroku::Auth.http_git_host}/#{info['name']}.git" else info["git_url"] diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index d84b14504..c7fa5c409 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -64,6 +64,7 @@ def remote_name def git_url app_info = api.get_app(app).body if options[:http_git] + warn_if_netrc_does_not_have_https_git "https://#{Heroku::Auth.http_git_host}/#{app_info['name']}.git" else app_info['git_url'] diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index fb3b41b50..a8f62f856 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -527,5 +527,11 @@ def app_owner email org?(email) ? email.gsub(/^(.*)@#{org_host}$/,'\1') : email end + def warn_if_netrc_does_not_have_https_git + unless Auth.netrc["git.heroku.com"] + warn "WARNING: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" + exit 1 + end + end end end From d9e170ac1294f0779faf453e8bde670ab0a44708 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 19 Nov 2014 21:30:50 -0800 Subject: [PATCH 146/952] just use heroku as bin name when updating since $0 is flaky on windows --- lib/heroku/updater.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index ea4afb237..fd4b645e0 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -170,13 +170,12 @@ def self.background_update! end log_path = File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate.log') FileUtils.mkdir_p File.dirname(log_path) - heroku_binary = File.expand_path($0) pid = if defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/ fork do - exec("\"#{heroku_binary}\" update &> #{log_path} 2>&1") + exec("heroku update &> #{log_path} 2>&1") end else - spawn("\"#{heroku_binary}\" update", {:err => log_path, :out => log_path}) + spawn("heroku update", {:err => log_path, :out => log_path}) end Process.detach(pid) FileUtils.mkdir_p File.dirname(last_autoupdate_path) From e5a8b37ea7d6e1533bd626fb5727fafbe2ae8254 Mon Sep 17 00:00:00 2001 From: unak Date: Thu, 20 Nov 2014 16:45:22 +0900 Subject: [PATCH 147/952] Make runnable without readline Readline does not always exist. --- lib/heroku/command/run.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 92c1d238f..04ee604be 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -1,4 +1,20 @@ -require "readline" +begin + require "readline" +rescue LoadError + module Readline + def self.readline(prompt) + print prompt + $stdout.flush + gets + end + + module HISTORY + def self.push(cmd) + # dummy + end + end + end +end require "heroku/command/base" require "heroku/helpers/log_displayer" From f856c30efb73b46f74d35ed4824f3f503abb9519 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 20 Nov 2014 13:40:26 -0800 Subject: [PATCH 148/952] v3.16.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 45587facd..e16936802 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.16.0 2014-11-20 +================= +Fixed update spawn command on some windows installs +Added warning for https git when netrc doesn't have credentials +Made runnable without readline + 3.15.3 2014-11-10 ================= Removed bamboo from stack command diff --git a/Gemfile.lock b/Gemfile.lock index 6546d91e3..8007b0528 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.15.3) + heroku (3.16.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3c582118a..092deeea7 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.15.3" + VERSION = "3.16.0" end From b1ea1b015a16bebff5a5fc4fc30d44880f1fd3be Mon Sep 17 00:00:00 2001 From: Josh Kalderimis Date: Thu, 20 Nov 2014 16:52:05 -0500 Subject: [PATCH 149/952] Use the new build env on Travis as an added bonus, caching works on the new platform --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index a0c8e510f..32cd0e19e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,14 @@ language: ruby + rvm: - 1.8.7 - 1.9.2 - 1.9.3 - 2.0.0 - 2.1.2 + +sudo: false + cache: bundler before_script: From a0be3b0d515bd2083e1bffa0423e4010ebd44091 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 20 Nov 2014 14:23:48 -0800 Subject: [PATCH 150/952] switched to dots instead of documentation format in travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 32cd0e19e..1e830c789 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ before_script: - git config --global user.email "bot@heroku.com" - git config --global user.name "Heroku Bot (Travis CI)" -script: bundle exec rspec spec --color --format documentation +script: bundle exec rspec spec --color deploy: provider: rubygems From a9625f04e010289a017c8c2fc3b3eba6bb690a82 Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Fri, 21 Nov 2014 21:55:33 -0500 Subject: [PATCH 151/952] Handle nils when trying to truncate text Some releases have a nil description, and the call to truncate that is raising --- lib/heroku/helpers.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index a8f62f856..39a336169 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -117,6 +117,7 @@ def time_ago(since) end def truncate(text, length) + return "" if text.nil? if text.size > length text[0, length - 2] + '..' else From 2c2f6e28b4e7a4163720cdf8a7b7965e17201b38 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 23 Nov 2014 19:31:03 -0800 Subject: [PATCH 152/952] v3.16.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e16936802..9dd102000 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.16.1 2014-11-23 +================= +Fixed bug with nil release description + 3.16.0 2014-11-20 ================= Fixed update spawn command on some windows installs diff --git a/Gemfile.lock b/Gemfile.lock index 8007b0528..0e87369f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.16.0) + heroku (3.16.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 092deeea7..b7d38a27b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.16.0" + VERSION = "3.16.1" end From e28b593c44bd16ae0258ea0aeee5d293ccc38c3e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 23 Nov 2014 19:37:37 -0800 Subject: [PATCH 153/952] clean before releasing --- Rakefile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index f53065d60..61c63d1b8 100644 --- a/Rakefile +++ b/Rakefile @@ -11,8 +11,13 @@ end Dir.glob('tasks/helpers/*.rb').each { |r| import r } Dir.glob('tasks/*.rake').each { |r| import r } +desc "clean" +task :clean do + rm_r "dist" +end + desc "release v#{version}" -task "release" => ["can_release", "build", "tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do +task "release" => ["can_release", "clean", "build", "tgz:release", "zip:release", "manifest:update", "deb:release", "gem:release", "git:tag"] do puts("released v#{version}") end From 35dcc9d813d301f6d73dfd13e6c607b6c67a711e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 23 Nov 2014 19:38:53 -0800 Subject: [PATCH 154/952] v3.16.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9dd102000..934fb1652 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.16.2 2014-11-23 +================= +Clean build dist directory before releasing + 3.16.1 2014-11-23 ================= Fixed bug with nil release description diff --git a/Gemfile.lock b/Gemfile.lock index 0e87369f3..c34860f1c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.16.1) + heroku (3.16.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index b7d38a27b..40ecb90c6 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.16.1" + VERSION = "3.16.2" end From a6df434e7e7b6df281529eb00cbc56e6d3c17776 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 23 Nov 2014 19:44:24 -0800 Subject: [PATCH 155/952] create dist folder after cleaning it --- Rakefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Rakefile b/Rakefile index 61c63d1b8..dbdbd0817 100644 --- a/Rakefile +++ b/Rakefile @@ -14,6 +14,7 @@ Dir.glob('tasks/*.rake').each { |r| import r } desc "clean" task :clean do rm_r "dist" + mkdir "dist" end desc "release v#{version}" From 48f5f0e21849906a07e2d9fa379a20ce6b1b1747 Mon Sep 17 00:00:00 2001 From: Jon Mountjoy Date: Mon, 24 Nov 2014 12:13:50 +0000 Subject: [PATCH 156/952] Fix error message to use the word "dyno" instead of the incorrect word "process". Also point to a useful follow up, instead of leaving the user flailing in the dark. --- lib/heroku/command/run.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 04ee604be..32be316f2 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -147,11 +147,11 @@ def rendezvous_session(rendezvous_url, &on_connect) rendezvous.on_connect(&on_connect) rendezvous.start rescue Timeout::Error, Errno::ETIMEDOUT - error "\nTimeout awaiting process" + error "\nTimeout awaiting dyno, see https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process" rescue OpenSSL::SSL::SSLError error "Authentication error" rescue Errno::ECONNREFUSED, Errno::ECONNRESET - error "\nError connecting to process" + error "\nError connecting to dyno, see https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process" rescue Interrupt ensure set_buffer(true) From caf14039ca3f89192d1aa456cd0de50f670d0827 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 24 Nov 2014 13:39:25 -0800 Subject: [PATCH 157/952] reduce update check duration to 10 minutes --- lib/heroku/updater.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index fd4b645e0..04aa68483 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -164,9 +164,9 @@ def self.last_autoupdate_path end def self.background_update! - # if we've updated in the last day, don't try again + # if we've updated in the last 10 minutes, don't try again if File.exists?(last_autoupdate_path) - return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60*24 + return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*10 end log_path = File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate.log') FileUtils.mkdir_p File.dirname(log_path) From c01de19916d45e226292fe3190eb0df022ab4ede Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Tue, 25 Nov 2014 14:11:00 -0800 Subject: [PATCH 158/952] remove special error handler from addons these are here for historical reasons; the generic handlers under Heroku::Command.prepare_run are going to do a much better job now. more specifically this will make sure addons commands are going to follow generic auth rules like requesting for 2fa --- lib/heroku/command/addons.rb | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 300d3807b..865b82702 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -249,14 +249,6 @@ def addon_run status [ release, price ].compact.join(' ') { :attachment => attachment, :message => message } - rescue RestClient::ResourceNotFound => e - error Heroku::Command.extract_error(e.http_body) { - e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - } - rescue RestClient::Locked => ex - raise - rescue RestClient::RequestFailed => e - error Heroku::Command.extract_error(e.http_body) end def configure_addon(label, &install_or_upgrade) From da3a77bdc7473c652daf2d0074f653b562ffb286 Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Thu, 27 Nov 2014 10:43:20 +0100 Subject: [PATCH 159/952] Fixed typos --- lib/heroku/client.rb | 2 +- lib/heroku/command/pgbackups.rb | 2 +- spec/heroku/command/apps_spec.rb | 2 +- spec/heroku/command/help_spec.rb | 2 +- spec/heroku/command/pg_spec.rb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index f71bf261a..353aa5da9 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -225,7 +225,7 @@ def remove_all_keys delete("/user/keys").to_s end - # Retreive ps list for the given app name. + # Retrieve ps list for the given app name. def ps(app_name) deprecate # 07/31/2012 json_decode get("/apps/#{app_name}/ps", :accept => 'application/json').to_s diff --git a/lib/heroku/command/pgbackups.rb b/lib/heroku/command/pgbackups.rb index 839e235d2..4cd4154bc 100644 --- a/lib/heroku/command/pgbackups.rb +++ b/lib/heroku/command/pgbackups.rb @@ -225,7 +225,7 @@ def transfer opts = {} verify_app = to.app || app - if confirm_command(verify_app, "WARNING: Destructive Action\nTransfering data from #{from.name} to #{to.name}") + if confirm_command(verify_app, "WARNING: Destructive Action\nTransferring data from #{from.name} to #{to.name}") backup = transfer!(from.url, from.name, to.url, to.name, opts) backup = poll_transfer!(backup) diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 53ca9f676..5f27cf7e5 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -19,7 +19,7 @@ module Heroku::Command api.delete_app("example") end - it "displays impicit app info" do + it "displays implicit app info" do stderr, stdout = execute("apps:info") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT diff --git a/spec/heroku/command/help_spec.rb b/spec/heroku/command/help_spec.rb index 5f1918a93..39e63bbef 100644 --- a/spec/heroku/command/help_spec.rb +++ b/spec/heroku/command/help_spec.rb @@ -13,7 +13,7 @@ expect(stdout).to include "help" end - it "should show command help and namespace help when ambigious" do + it "should show command help and namespace help when ambiguous" do stderr, stdout = execute("help apps") expect(stderr).to eq("") expect(stdout).to include "heroku apps" diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 7bff764c9..1e437ec14 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -33,7 +33,7 @@ module Heroku::Command 'app' => {'name' => 'sushi'}, 'name' => 'HEROKU_POSTGRESQL_FOLLOW', 'config_var' => 'HEROKU_POSTGRESQL_FOLLOW_URL', - 'resource' => {'name' => 'whatever-somethign-2323', + 'resource' => {'name' => 'whatever-something-2323', 'value' => 'postgres://follow_database_url', 'type' => 'heroku-postgresql:ronin' }}) ]) From 36a1594099ad01b45e5695fcb59bee252bd49106 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 26 Nov 2014 14:02:12 -0800 Subject: [PATCH 160/952] default to http git --- lib/heroku/auth.rb | 8 ---- lib/heroku/command/apps.rb | 34 ++++++++--------- lib/heroku/command/base.rb | 9 +++++ lib/heroku/command/git.rb | 24 +++++------- lib/heroku/helpers.rb | 2 +- spec/heroku/auth_spec.rb | 64 -------------------------------- spec/heroku/command/apps_spec.rb | 42 ++++++++++----------- spec/heroku/command/base_spec.rb | 8 ++-- spec/heroku/command/git_spec.rb | 16 ++++---- spec/spec_helper.rb | 2 + 10 files changed, 70 insertions(+), 139 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 3521f2d0f..b240a87ff 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -261,7 +261,6 @@ def ask_for_and_save_credentials @credentials = ask_for_credentials write_credentials check - check_for_associated_ssh_key unless Heroku::Command.current_command == "keys:add" @credentials rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e delete_credentials @@ -273,13 +272,6 @@ def ask_for_and_save_credentials raise e end - def check_for_associated_ssh_key - if api.get_keys.body.empty? - display "Your Heroku account does not have a public ssh key uploaded." - associate_or_generate_ssh_key - end - end - def associate_or_generate_ssh_key unless File.exists?("#{home_directory}/.ssh/id_rsa.pub") display "Could not find an existing public key at ~/.ssh/id_rsa.pub" diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index a6947386e..7ab1b4e3f 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -84,12 +84,12 @@ def index # # $ heroku apps:info # === example - # Git URL: git@heroku.com:example.git + # Git URL: https://git.heroku.com/example.git # Repo Size: 5M # ... # # $ heroku apps:info --shell - # git_url=git@heroku.com:example.git + # git_url=https://git.heroku.com/example.git # repo_size=5000000 # ... # @@ -111,6 +111,7 @@ def info end if options[:shell] + app_data['git_url'] = git_url(app_data['name']) if app_data['domain_name'] app_data['domain_name'] = app_data['domain_name']['domain'] end @@ -152,7 +153,7 @@ def info data["Database Size"] = format_bytes(app_data["database_size"]) end - data["Git URL"] = app_data["git_url"] + data["Git URL"] = git_url(app_data['name']) if app_data["database_tables"] data["Database Size"].gsub!('(empty)', '0K') + " in #{quantify("table", app_data["database_tables"])}" @@ -195,6 +196,7 @@ def info # -s, --stack STACK # the stack on which to create the app # --region REGION # specify region for this app to run in # -l, --locked # lock the app + # --ssh-git # Use SSH git protocol # -t, --tier TIER # HIDDEN: the tier for this app # --http-git # HIDDEN: Use HTTP git protocol # @@ -202,16 +204,16 @@ def info # # $ heroku apps:create # Creating floating-dragon-42... done, stack is cedar - # http://floating-dragon-42.heroku.com/ | git@heroku.com:floating-dragon-42.git + # http://floating-dragon-42.heroku.com/ | https://git.heroku.com/floating-dragon-42.git # # $ heroku apps:create -s bamboo # Creating floating-dragon-42... done, stack is bamboo-mri-1.9.2 - # http://floating-dragon-42.herokuapp.com/ | git@heroku.com:floating-dragon-42.git + # http://floating-dragon-42.herokuapp.com/ | https://git.heroku.com/floating-dragon-42.git # # # specify a name # $ heroku apps:create example # Creating example... done, stack is cedar - # http://example.heroku.com/ | git@heroku.com:example.git + # http://example.heroku.com/ | https://git.heroku.com/example.git # # # create a staging app # $ heroku apps:create example-staging --remote staging @@ -237,13 +239,6 @@ def create api.post_app(params).body end - git_url = if options[:http_git] - warn_if_netrc_does_not_have_https_git - "https://#{Heroku::Auth.http_git_host}/#{info['name']}.git" - else - info["git_url"] - end - begin action("Creating #{info['name']}", :org => !!org) do if info['create_status'] == 'creating' @@ -274,13 +269,13 @@ def create display("BUILDPACK_URL=#{buildpack}") end - hputs([ info["web_url"], git_url ].join(" | ")) + hputs([ info["web_url"], git_url(info['name']) ].join(" | ")) rescue Timeout::Error hputs("Timed Out! Run `heroku status` to check for known platform issues.") end unless options[:no_remote].is_a? FalseClass - create_git_remote(options[:remote] || "heroku", git_url) + create_git_remote(options[:remote] || "heroku", git_url(info['name'])) end end @@ -290,10 +285,13 @@ def create # # rename the app # + # --ssh-git # Use SSH git protocol + # --http-git # HIDDEN: Use HTTP git protocol + # #Example: # # $ heroku apps:rename example-newname - # http://example-newname.herokuapp.com/ | git@heroku.com:example-newname.git + # http://example-newname.herokuapp.com/ | https://git.heroku.com/example-newname.git # Git remote heroku updated # def rename @@ -308,13 +306,13 @@ def rename end app_data = api.get_app(newname).body - hputs([ app_data["web_url"], app_data["git_url"] ].join(" | ")) + hputs([ app_data["web_url"], git_url(newname) ].join(" | ")) if remotes = git_remotes(Dir.pwd) remotes.each do |remote_name, remote_app| next if remote_app != app git "remote rm #{remote_name}" - git "remote add #{remote_name} #{app_data["git_url"]}" + git "remote add #{remote_name} #{git_url(newname)}" hputs("Git remote #{remote_name} updated") end else diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index a7a797b77..f549059ea 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -260,6 +260,15 @@ def skip_org? !%w{default production prod}.include? ENV['HEROKU_CLOUD'] end + def git_url(app_name) + if options[:ssh_git] + "git@#{Heroku::Auth.git_host}:#{app_name}.git" + else + warn_if_netrc_does_not_have_https_git + "https://#{Heroku::Auth.http_git_host}/#{app_name}.git" + end + end + def git_remotes(base_dir=Dir.pwd) remotes = {} original_dir = Dir.pwd diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index c7fa5c409..73a2308d9 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -11,7 +11,8 @@ class Heroku::Command::Git < Heroku::Command::Base # clones a heroku app to your local machine at DIRECTORY (defaults to app name) # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --http-git # use HTTP git protocol + # --ssh-git # use SSH git protocol + # --http-git # HIDDEN: Use HTTP git protocol # # #Examples: @@ -26,9 +27,10 @@ def clone name = options[:app] || shift_argument || error("Usage: heroku git:clone APP [DIRECTORY]") directory = shift_argument validate_arguments! + app_info = api.get_app(app).body puts "Cloning from app '#{name}'..." - system "git clone -o #{remote_name} #{git_url} #{directory}".strip + system "git clone -o #{remote_name} #{git_url(app_info['name'])} #{directory}".strip end alias_command "clone", "git:clone" @@ -40,7 +42,8 @@ def clone # if OPTIONS are specified they will be passed to git remote add # # -r, --remote REMOTE # the git remote to create, default "heroku" - # --http-git # use HTTP git protocol + # --ssh-git # use SSH git protocol + # --http-git # HIDDEN: Use HTTP git protocol # #Examples: # @@ -48,10 +51,11 @@ def clone # Git remote heroku added # def remote + app_info = api.get_app(app).body if git('remote').split("\n").include?(remote_name) - update_git_remote(remote_name, git_url) + update_git_remote(remote_name, git_url(app_info['name'])) else - create_git_remote(remote_name, git_url) + create_git_remote(remote_name, git_url(app_info['name'])) end end @@ -60,14 +64,4 @@ def remote def remote_name options[:remote] || DEFAULT_REMOTE_NAME end - - def git_url - app_info = api.get_app(app).body - if options[:http_git] - warn_if_netrc_does_not_have_https_git - "https://#{Heroku::Auth.http_git_host}/#{app_info['name']}.git" - else - app_info['git_url'] - end - end end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 39a336169..6945a1843 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -529,7 +529,7 @@ def app_owner email end def warn_if_netrc_does_not_have_https_git - unless Auth.netrc["git.heroku.com"] + unless Auth.netrc && Auth.netrc["git.heroku.com"] warn "WARNING: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" exit 1 end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index b2048b51d..edd8e9ec5 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -87,7 +87,6 @@ module Heroku before do allow(@cli).to receive(:ask_for_credentials).and_return(['new_user', 'new_password']) allow(@cli).to receive(:check) - expect(@cli).to receive(:check_for_associated_ssh_key) @cli.reauthorize end it "updates saved credentials" do @@ -122,7 +121,6 @@ module Heroku it "asks for credentials when the file doesn't exist" do @cli.delete_credentials expect(@cli).to receive(:ask_for_credentials).and_return(["u", "p"]) - expect(@cli).to receive(:check_for_associated_ssh_key) expect(@cli.user).to eq('u') expect(@cli.password).to eq('p') end @@ -132,7 +130,6 @@ module Heroku allow(@cli).to receive(:check) allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") expect(@cli).to receive(:write_credentials) - expect(@cli).to receive(:check_for_associated_ssh_key) @cli.ask_for_and_save_credentials end @@ -167,7 +164,6 @@ module Heroku it "writes the login information to the credentials file for the 'heroku login' command" do allow(@cli).to receive(:ask_for_credentials).and_return(['one', 'two']) allow(@cli).to receive(:check) - expect(@cli).to receive(:check_for_associated_ssh_key) @cli.reauthorize expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['one', 'two']) end @@ -182,65 +178,5 @@ module Heroku expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"]).to eq(["user", api_key[0,40]]) end end - - describe "automatic key uploading" do - before(:each) do - allow(@cli).to receive(:home_directory).and_return(Heroku::Helpers.home_directory) - FileUtils.mkdir_p("#{@cli.home_directory}/.ssh") - allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") - end - - describe "an account with existing keys" do - before :each do - @api = double(Object) - @response = double(Object) - expect(@response).to receive(:body).and_return(['existingkeys']) - expect(@api).to receive(:get_keys).and_return(@response) - expect(@cli).to receive(:api).and_return(@api) - end - - it "should not do anything if the account already has keys" do - expect(@cli).not_to receive(:associate_key) - @cli.check_for_associated_ssh_key - end - end - - describe "an account with no keys" do - before :each do - @api = double(Object) - @response = double(Object) - expect(@response).to receive(:body).and_return([]) - expect(@api).to receive(:get_keys).and_return(@response) - expect(@cli).to receive(:api).and_return(@api) - end - - describe "with zero public keys" do - it "should ask to generate a key" do - expect(@cli).to receive(:ask).and_return("y") - expect(@cli).to receive(:generate_ssh_key).with("#{@cli.home_directory}/.ssh/id_rsa") - expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa.pub") - @cli.check_for_associated_ssh_key - end - end - - describe "with many public keys" do - before :each do - FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa.pub") - FileUtils.touch("#{@cli.home_directory}/.ssh/id_rsa2.pub") - end - - after :each do - FileUtils.rm_rf(@cli.home_directory) - end - - it "should ask which key to upload" do - File.open("#{@cli.home_directory}/.ssh/id_rsa.pub", "w") { |f| f.puts } - expect(@cli).to receive(:associate_key).with("#{@cli.home_directory}/.ssh/id_rsa2.pub") - expect(@cli).to receive(:ask).and_return("2") - @cli.check_for_associated_ssh_key - end - end - end - end end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 5f27cf7e5..e51597fce 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -24,7 +24,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === example -Git URL: git@heroku.com:example.git +Git URL: https://git.heroku.com/example.git Owner Email: email@example.com Stack: cedar Web URL: http://example.herokuapp.com/ @@ -36,7 +36,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === example -Git URL: git@heroku.com:example.git +Git URL: https://git.heroku.com/example.git Owner Email: email@example.com Stack: cedar Web URL: http://example.herokuapp.com/ @@ -50,7 +50,7 @@ module Heroku::Command create_status=complete created_at=\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4} dynos=0 -git_url=git@heroku.com:example.git +git_url=https://git.heroku.com/example.git id=\\d{1,5} name=example owner_email=email@example.com @@ -76,7 +76,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating #{name}... done, stack is bamboo-mri-1.9.2 -http://#{name}.herokuapp.com/ | git@heroku.com:#{name}.git +http://#{name}.herokuapp.com/ | https://git.heroku.com/#{name}.git Git remote heroku added STDOUT end @@ -89,7 +89,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote heroku added STDOUT end @@ -102,7 +102,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote heroku added STDOUT end @@ -115,7 +115,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git STDOUT end api.delete_app("example") @@ -129,7 +129,7 @@ module Heroku::Command Creating addonapp... done, stack is bamboo-mri-1.9.2 Adding custom_domains:basic to addonapp... done Adding releases:basic to addonapp... done -http://addonapp.herokuapp.com/ | git@heroku.com:addonapp.git +http://addonapp.herokuapp.com/ | https://git.heroku.com/addonapp.git Git remote heroku added STDOUT end @@ -143,7 +143,7 @@ module Heroku::Command expect(stdout).to eq <<-STDOUT Creating buildpackapp... done, stack is bamboo-mri-1.9.2 BUILDPACK_URL=http://example.org/buildpack.git -http://buildpackapp.herokuapp.com/ | git@heroku.com:buildpackapp.git +http://buildpackapp.herokuapp.com/ | https://git.heroku.com/buildpackapp.git Git remote heroku added STDOUT end @@ -156,7 +156,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating alternate-remote... done, stack is bamboo-mri-1.9.2 -http://alternate-remote.herokuapp.com/ | git@heroku.com:alternate-remote.git +http://alternate-remote.herokuapp.com/ | https://git.heroku.com/alternate-remote.git Git remote alternate added STDOUT end @@ -267,7 +267,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Renaming example to example2... done -http://example2.herokuapp.com/ | git@heroku.com:example2.git +http://example2.herokuapp.com/ | https://git.heroku.com/example2.git Don't forget to update your Git remotes on any local checkouts. STDOUT end @@ -341,7 +341,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote heroku added STDOUT expect(`git remote`.strip).to match(/^heroku$/) @@ -355,7 +355,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git Git remote myremote added STDOUT expect(`git remote`.strip).to match(/^myremote$/) @@ -370,7 +370,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating example... done, stack is bamboo-mri-1.9.2 -http://example.herokuapp.com/ | git@heroku.com:example.git +http://example.herokuapp.com/ | https://git.heroku.com/example.git STDOUT api.delete_app("example") end @@ -379,8 +379,8 @@ module Heroku::Command it "renames updating the corresponding heroku git remote" do with_blank_git_repository do `git remote add github git@github.com:test/test.git` - `git remote add production git@heroku.com:example.git` - `git remote add staging git@heroku.com:example-staging.git` + `git remote add production https://git.heroku.com/example.git` + `git remote add staging https://git.heroku.com/example-staging.git` api.post_app("name" => "example", "stack" => "cedar") stderr, stdout = execute("apps:rename example2") @@ -390,17 +390,17 @@ module Heroku::Command expect(remotes).to eq <<-REMOTES github\tgit@github.com:test/test.git (fetch) github\tgit@github.com:test/test.git (push) -production\tgit@heroku.com:example2.git (fetch) -production\tgit@heroku.com:example2.git (push) -staging\tgit@heroku.com:example-staging.git (fetch) -staging\tgit@heroku.com:example-staging.git (push) +production\thttps://git.heroku.com/example2.git (fetch) +production\thttps://git.heroku.com/example2.git (push) +staging\thttps://git.heroku.com/example-staging.git (fetch) +staging\thttps://git.heroku.com/example-staging.git (push) REMOTES end end it "destroys removing any remotes pointing to the app" do with_blank_git_repository do - `git remote add heroku git@heroku.com:example.git` + `git remote add heroku https://git.heroku.com/example.git` api.post_app("name" => "example", "stack" => "cedar") stderr, stdout = execute("apps:destroy --confirm example") diff --git a/spec/heroku/command/base_spec.rb b/spec/heroku/command/base_spec.rb index 8b1cfec20..779564558 100644 --- a/spec/heroku/command/base_spec.rb +++ b/spec/heroku/command/base_spec.rb @@ -71,10 +71,10 @@ module Heroku::Command allow(Dir).to receive(:chdir) expect(File).to receive(:exists?).with(".git").and_return(true) expect(@base).to receive(:git).with('remote -v').and_return(<<-REMOTES) -staging\tgit@heroku.com:example-staging.git (fetch) -staging\tgit@heroku.com:example-staging.git (push) -production\tgit@heroku.com:example.git (fetch) -production\tgit@heroku.com:example.git (push) +staging\thttps://git.heroku.com/example-staging.git (fetch) +staging\thttps://git.heroku.com/example-staging.git (push) +production\thttps://git.heroku.com/example.git (fetch) +production\thttps://git.heroku.com/example.git (push) other\tgit@other.com:other.git (fetch) other\tgit@other.com:other.git (push) REMOTES diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb index 132938e7e..9d337f7f5 100644 --- a/spec/heroku/command/git_spec.rb +++ b/spec/heroku/command/git_spec.rb @@ -20,7 +20,7 @@ module Heroku::Command it "clones and adds remote" do any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git") do + mock(git).system("git clone -o heroku https://git.heroku.com/example.git") do puts "Cloning into 'example'..." end end @@ -34,7 +34,7 @@ module Heroku::Command it "clones into another dir" do any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git somedir") do + mock(git).system("git clone -o heroku https://git.heroku.com/example.git somedir") do puts "Cloning into 'somedir'..." end end @@ -48,7 +48,7 @@ module Heroku::Command it "can specify app with -a" do any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git") do + mock(git).system("git clone -o heroku https://git.heroku.com/example.git") do puts "Cloning into 'example'..." end end @@ -62,7 +62,7 @@ module Heroku::Command it "can specify app with -a and a dir" do any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku git@heroku.com:example.git somedir") do + mock(git).system("git clone -o heroku https://git.heroku.com/example.git somedir") do puts "Cloning into 'somedir'..." end end @@ -76,7 +76,7 @@ module Heroku::Command it "clones and sets -r remote" do any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o other git@heroku.com:example.git") do + mock(git).system("git clone -o other https://git.heroku.com/example.git") do puts "Cloning into 'example'..." end end @@ -106,7 +106,7 @@ module Heroku::Command it "adds remote" do any_instance_of(Heroku::Command::Git) do |git| stub(git).git('remote').returns("origin") - stub(git).git('remote add heroku git@heroku.com:example.git') + stub(git).git('remote add heroku https://git.heroku.com/example.git') end stderr, stdout = execute("git:remote") expect(stderr).to eq("") @@ -118,7 +118,7 @@ module Heroku::Command it "adds -r remote" do any_instance_of(Heroku::Command::Git) do |git| stub(git).git('remote').returns("origin") - stub(git).git('remote add other git@heroku.com:example.git') + stub(git).git('remote add other https://git.heroku.com/example.git') end stderr, stdout = execute("git:remote -r other") expect(stderr).to eq("") @@ -130,7 +130,7 @@ module Heroku::Command it "updates remote when it already exists" do any_instance_of(Heroku::Command::Git) do |git| stub(git).git('remote').returns("heroku") - stub(git).git('remote set-url heroku git@heroku.com:example.git') + stub(git).git('remote set-url heroku https://git.heroku.com/example.git') end stderr, stdout = execute("git:remote") expect(stderr).to eq("") diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 04956a28d..60987bd60 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -203,6 +203,8 @@ module Heroku::Helpers def home_directory @home_directory end + undef_method :warn_if_netrc_does_not_have_https_git + def warn_if_netrc_does_not_have_https_git; end end require "support/display_message_matcher" From 4cb96d6ce639ef5bfad080667ffaa57f7402dc85 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 2 Dec 2014 23:57:24 -0800 Subject: [PATCH 161/952] added windows builder --- .gitignore | 2 + resources/exe/foreman | 9 + resources/exe/foreman.bat | 11 ++ resources/exe/heroku | 29 ++++ .../exe/heroku-codesign-cert.encrypted.pvk | Bin 0 -> 1212 bytes resources/exe/heroku-codesign-cert.spc | Bin 0 -> 1745 bytes resources/exe/heroku.bat | 11 ++ resources/exe/heroku.iss | 76 +++++++++ resources/exe/ssh-keygen.bat | 3 + tasks/exe.rake | 158 ++++++++++++++++++ 10 files changed, 299 insertions(+) create mode 100755 resources/exe/foreman create mode 100644 resources/exe/foreman.bat create mode 100755 resources/exe/heroku create mode 100644 resources/exe/heroku-codesign-cert.encrypted.pvk create mode 100644 resources/exe/heroku-codesign-cert.spc create mode 100644 resources/exe/heroku.bat create mode 100644 resources/exe/heroku.iss create mode 100644 resources/exe/ssh-keygen.bat create mode 100644 tasks/exe.rake diff --git a/.gitignore b/.gitignore index a25a89285..eebb386cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ /tags /vendor /.rbenv-version +/.cache +/resources/exe/heroku-codesign-cert.pvk diff --git a/resources/exe/foreman b/resources/exe/foreman new file mode 100755 index 000000000..2fee762fe --- /dev/null +++ b/resources/exe/foreman @@ -0,0 +1,9 @@ +#!/bin/sh +# find embedded ruby relative to script +bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` +exec "$bindir/ruby" -x "$0" "$@" + +#!/usr/bin/env ruby +# encoding: UTF-8 +require "foreman/cli" +Foreman::CLI.start diff --git a/resources/exe/foreman.bat b/resources/exe/foreman.bat new file mode 100644 index 000000000..f15c848c6 --- /dev/null +++ b/resources/exe/foreman.bat @@ -0,0 +1,11 @@ +:: Don't use ECHO OFF to avoid possible change of ECHO +:: Use SETLOCAL so variables set in the script are not persisted +@SETLOCAL + +:: Add bundled ruby version to the PATH, use HerokuPath as starting point +@SET HEROKU_RUBY="%HerokuPath%\ruby-1.9.3\bin" +@SET PATH=%HEROKU_RUBY%;%PATH% + +:: Invoke 'foreman' (the calling script) as argument to ruby. +:: Also forward all the arguments provided to it. +@ruby.exe "%~dpn0" %* diff --git a/resources/exe/heroku b/resources/exe/heroku new file mode 100755 index 000000000..7a7fd4ce5 --- /dev/null +++ b/resources/exe/heroku @@ -0,0 +1,29 @@ +#!/bin/sh +# find embedded ruby relative to script +bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` +exec "$bindir/ruby" -x "$0" "$@" + +#!/usr/bin/env ruby +# encoding: UTF-8 + +# resolve bin path, ignoring symlinks +require "pathname" +bin_file = Pathname.new(__FILE__).realpath + +# add locally vendored gems to libpath +gem_dir = File.expand_path("../../vendor/gems", bin_file) +Dir["#{gem_dir}/**/lib"].each do |libdir| + $:.unshift libdir +end + +# add self to libpath +$:.unshift File.expand_path("../../lib", bin_file) + +# inject any code in ~/.heroku/client over top +require "heroku/updater" +Heroku::Updater.inject_libpath + +# start up the CLI +require "heroku/cli" +Heroku.user_agent = "heroku/toolbelt/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" +Heroku::CLI.start(*ARGV) diff --git a/resources/exe/heroku-codesign-cert.encrypted.pvk b/resources/exe/heroku-codesign-cert.encrypted.pvk new file mode 100644 index 0000000000000000000000000000000000000000..d8e6ca57386a3bc2bb37df27c41f24ab3f06fbf2 GIT binary patch literal 1212 zcmV;t1Vj5C@wKo300001000010000G0001#1ONckfc6w8qCo(#OO3)qPMVGf0ssI2 zqyPX9W4*UY?L zgmdke45kI%gJ5B(jDa`)%?_8J1wn`+vxVwbIn@$H()CPOSoUsl7xgmUZR`SfgTzTQ z48UywV${B}s|CNH2Bvg#=y3*>Z5b--w)_FAe}nNIMUa#$>og4L78b$-N|Lw_9;6;Q zlT_c5tA!W#Ccc*D`^3dhk4XM==lw%$d*Zc%X&^{*+RACbwM34;OO zG?C;0rL+aS^+zLi6J;0SC^_zDQGMyyqAYdVlCS|dBRf z_)GKM8q$HT5N4uQp*~pQV7_}l(TUMo4D@k^*oB%GeKTa1rY&sgzcLKJA}W3u_59Wu z7Y4&FOf+Ni6{8;p7%OT~Via2YY+B>0g)H(@CEwWDYnbBOC=IwFE#zy+yh&~|hl*fa zLecWqVlBqR0VY6tM!|+}4Sr<6)KYt=1UMWD5%_`_fL*=+;>G*DLQ=dG8C zz{;VgJfR!)*J+{CHn`;7N!Xv^3Tyrwr%cSYskXHw0ieCX-=aC1^0Vtizv79Dn%0)n ztY4O50)nZ`vz)O-RmueMETFPqEs3zk&vFC6725IPXGTnad6_9@BKP60Qm?|WO%Qz) zH~kHjL+UQw=uCRldrgc@P1u+pUIVdY^Un{^lJSxc;6m$0tuiAG#&M*IU2j!F$tmyQ zow@A4H`d)kJZm3$cl{D)%z5Nyc$rhd#CniYT^it9sL{t}AM$B4M-+hh?UcGCaNK8g z6fA46&^|-^o+iQN;njGU-f((5E+AD4Jdl;zEj3x#XN?)vcj?lr>dNRVhP$xT2!;!( zAS6=AdSCC%6jME{-{XPLp#tcGa@|W|a=Y6q;>TTDPcc^1b}c1Cp{cvF8}vCJ zWv=LiHQW1GBt6ZqF2fyM!P3_sTS6dtmh1_{Xvc=6>0M&@_zwE2$~dbh=c1%l2_4if z^SS=l^wuZ8L8TGn7b~;|T}g;K?&a7Y4wm$%`xLDdmZ0nbWPs{m?l~0RF(wc4i5omp z+C{8jWUqV13Hk=xyx3_ literal 0 HcmV?d00001 diff --git a/resources/exe/heroku-codesign-cert.spc b/resources/exe/heroku-codesign-cert.spc new file mode 100644 index 0000000000000000000000000000000000000000..a5c57f3d5e547f5a54f34b12f562b087faba3418 GIT binary patch literal 1745 zcma)6e^3-v9Dloe+`)0jk6GkU{IQ|fWZd0b5KtVmNG1<8Nlr7R$>hRG(=kodniNZHhDw<-g#1Mg1-%CxLH*OtytnWB`F_6N z_jzx>`-P&kO&T`KYxMRh2^r}Vr6-^$Z6OJQ1u*;{4}m;B_@=#2%==up+&67khE>xeixZsNZmWVPw*q{tCXbPscE{%dn+4 z`tS?FV3oBsEis3@rdmb&JKBtMk5(7$O=~~3#FM%Bs;YkTkwdvB=gzK|ZJ6G9%^I%jY{Mebo&N?#pK^(MzkqAF9{(sbJy#h=cyp=-Usw%uM9^vNh+k zwmh%+c>l7VNxN?r2#&3XqMN()?Olsh^n;DfeLvoa2s2OUco6nj$bhADVxK8{AfS7w z8pwXU`R2t9MSFj0O3T0VZg$;K&9@eD@XnIRO2@s4C7D0maOr*5PRt`I0uXyeYAzJ1 zR5%UY_>}3AI2gM&W_8KMCz?K2oyb|$d2_+l#=fk#Qj_5nOo))gN@D!HYe&Zlb09OL zf76-xiVM7MemRW5?nr_dlt?I2wsxq_rX)%fP%aI9Qi^I(Mp4j>Nz){D_$o~Cu|BO= zUE=Y$GPGJ=u&0cxJ1sX{WNO~6PKGSYX%OYVL9_oykVu@y+cFZ72%wLD42@=K*ehNm z;r+`_5y;#JGlC*XB33h^>EFMC*6p^dd6!!)m|ae{&Epg*5w#S>7mr4h$a-bRY)ycu+J(F`q*o9<5yn z;Sd9_$7aQr0mWF+21fVRSeiYs7oujCe1+2R@sCPJrj(^I`_pf;pJM zpe%TIY+FX~;j?PsIx%?71R*Jh9ylXtL&6(3^3j~U+cmBxVD2c3^gbOdHxBnV_c)?9 z*xyFlTaE!Yw~^k4TN|oC4^-e%qH&p%5R7dvsM(_SLICP&g7!Dzo3qB--AP|=zPEEt ze|W2?@mqH{);yFQTOU|jc*ks07}c4Bbrk+QS5`jjs$|3*)3RN~HMf0FOq4eZ~}x0pWh&pdMF qY;kw*E>*8Io>kb}uGc)8=X|a=uc7eQmW3U0tGDn^88 +AppVerName=Heroku Toolbelt <%= version %> +AppPublisher=Heroku, Inc. +AppPublisherURL=http://www.heroku.com/ +DefaultDirName={pf}\Heroku +DefaultGroupName=Heroku +Compression=lzma2 +SolidCompression=yes +OutputBaseFilename=<%= File.basename(exe_task.name, ".exe") %> +OutputDir=.. +ChangesEnvironment=yes +UsePreviousSetupType=no +AlwaysShowComponentsList=no +SignTool=mono-signcode + +; For Ruby expansion ~ 32MB (installed) - 12MB (installer) +ExtraDiskSpaceRequired=20971520 + +[Types] +Name: client; Description: "Full Installation"; +Name: custom; Description: "Custom Installation"; flags: iscustom + +[Components] +Name: "toolbelt"; Description: "Heroku Toolbelt"; Types: "client custom" +Name: "toolbelt/client"; Description: "Heroku Client"; Types: "client custom"; Flags: fixed +Name: "toolbelt/foreman"; Description: "Foreman"; Types: "client custom" +Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git.exe')" +Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git.exe')" + +[Files] +Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" +Source: "installers\rubyinstaller.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" +Source: "installers\git.exe"; DestDir: "{tmp}"; Components: "toolbelt/git" + +[Registry] +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "HerokuPath"; \ + ValueData: "{app}" +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ + ValueData: "{olddata};{app}\bin"; Check: NeedsAddPath(ExpandConstant('{app}\bin')) +Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ + ValueData: "{olddata};{pf}\git\cmd"; Check: NeedsAddPath(ExpandConstant('{pf}\git\cmd')) + +[Run] +Filename: "{tmp}\rubyinstaller.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-1.9.3"""; \ + Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" +Filename: "{app}\ruby-1.9.3\bin\gem.bat"; Parameters: "install foreman --no-rdoc --no-ri"; \ + Flags: runhidden shellexec waituntilterminated; StatusMsg: "Installing Foreman"; Components: "toolbelt/foreman" +Filename: "{tmp}\git.exe"; Parameters: "/silent /nocancel /noicons"; \ + Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" + +[Code] + +function NeedsAddPath(Param: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment', + 'Path', OrigPath) + then begin + Result := True; + exit; + end; + // look for the path with leading and trailing semicolon + // Pos() returns 0 if not found + Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; +end; + +function IsProgramInstalled(Name: string): boolean; +var + ResultCode: integer; +begin + Result := Exec(Name, 'version', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; diff --git a/resources/exe/ssh-keygen.bat b/resources/exe/ssh-keygen.bat new file mode 100644 index 000000000..cb16d2a30 --- /dev/null +++ b/resources/exe/ssh-keygen.bat @@ -0,0 +1,3 @@ +@SETLOCAL +@SET HOME=%USERPROFILE% +@"%HerokuPath%\..\Git\bin\ssh-keygen.exe" %* diff --git a/tasks/exe.rake b/tasks/exe.rake new file mode 100644 index 000000000..b235d1d6c --- /dev/null +++ b/tasks/exe.rake @@ -0,0 +1,158 @@ +require "erb" +require "shellwords" + +$is_mac = RUBY_PLATFORM =~ /darwin/ +$base_path = File.expand_path(File.join(File.dirname(__FILE__), "..")) +$cache_path = File.join($base_path, ".cache") +def windows_path(path); `winepath -w #{path.shellescape}`.chomp; end + +def setup_wine_env + ENV["WINEPREFIX"] = "#$base_path/dist/wine" # keep it contained; by default it goes in $HOME/.wine + ENV["WINEDEBUG"] = "-all" # wine is full of errors, no one cares + ENV["WINEDLLOVERRIDES"] = "winemenubuilder.exe=n" # tell wine to use our custom winemenubuilder.exe, see comment in exe:init-wine + ENV["DISPLAY"] = ':42' + $xvfb_pid = spawn 'Xvfb', ':42', [:out,:err] => '/dev/null' # use a virtual x server so we can run headless + sleep(2) # give Xvfb some time to boot up +end + +def cleanup_after_wine + # terminate our Xvfb process + sleep(2) # give Xvfb some time to finish up; seems to prevent some error messages + Process.kill "INT", $xvfb_pid + Process.wait $xvfb_pid + # wine leaves the terminal all sorts of broken. + # pretty much every time it'll switch input to cursor key application mode (cf. http://www.tldp.org/HOWTO/Keyboard-and-Console-HOWTO-21.html), + # fairly often it'll turn echo off, a couple other odd things have also been observed. + # this sends a soft reset to the terminal, albeit I suspect it only works in xterm emulators, + # but then again maybe it's an xterm-only problem anyway? who knows… + system "echo \033[!p" + system "stty echo" +end + +# ensure cleanup_after_wine runs when aborted too +trap("INT") { cleanup_after_wine; exit } + +# see comment on build_zip +def extract_zip(filename, destination) + tempdir do |dir| + sh %{ unzip -q "#{filename}" } + sh %{ mv * "#{destination}" } + end +end + +# a bunch of needed binaries are in an amazon bucket. not sure I love this, but I guess it keeps the repo small +def cache_file_from_bucket(filename) + FileUtils.mkdir_p $cache_path + file_cache_path = File.join($cache_path, filename) + system "curl -# http://heroku-toolbelt.s3.amazonaws.com/#{filename} -o '#{file_cache_path}'" unless File.exists? file_cache_path + file_cache_path +end + +# file task for the final windows installer file. +# if you ask me, it's fairly pointless to be using a file task for the final +# file if the intermediates get placed in all sorts of temp dirs that then get +# destroyed, so we don't get to benefit from the time savings of not generating +# the same thing over and over again. +file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| + tempdir do |build_path| + installer_path = "#{build_path}/heroku-installer" + heroku_cli_path = "#{installer_path}/heroku" + mkdir_p heroku_cli_path + extract_zip "#{$base_path}/dist/heroku-3.16.2.zip", "#{heroku_cli_path}/" + + # gather the ruby and git installers, downlading from s3 + mkdir "#{installer_path}/installers" + cd "#{installer_path}/installers" do + ["rubyinstaller.exe", "git.exe"].each { |i| cp cache_file_from_bucket(i), i } + end + + # add windows helper executables to the heroku cli + cp resource("exe/heroku.bat"), "#{heroku_cli_path}/bin/heroku.bat" + cp resource("exe/heroku"), "#{heroku_cli_path}/bin/heroku" + cp resource("exe/foreman.bat"), "#{heroku_cli_path}/bin/foreman.bat" + cp resource("exe/foreman"), "#{heroku_cli_path}/bin/foreman" + cp resource("exe/ssh-keygen.bat"), "#{heroku_cli_path}/bin/ssh-keygen.bat" + + # render the iss file used by inno setup to compile the installer + # this sets the version and the output filename + File.write("#{installer_path}/heroku.iss", ERB.new(File.read(resource("exe/heroku.iss"))).result(binding)) + + # the codesign command used by inno to sign the installer and uninstaller + sign_cmd = 'c:\windows\mono\mono-2.0\lib\mono\4.5\signcode.exe' + %Q[ + -spc "#{windows_path(resource('exe/heroku-codesign-cert.spc'))}" + -v "#{windows_path(resource('exe/heroku-codesign-cert.pvk'))}" + -a sha1 -$ commercial + -n "Heroku Toolbelt" + $f ] # $f gets replaced by iscc with the path to the file it wants to compile + .gsub("\n", ' ') # everything on a single line now + .gsub('"', '$q') # iscc requires quotes to be escaped this way, don't ask + + # compile installer under wine! + setup_wine_env + system 'wine', 'C:\inno\ISCC.exe', + "/Smono-signcode=#{sign_cmd}", '/qp', + windows_path("#{installer_path}/heroku.iss") + cleanup_after_wine + + # move final installer from build_path to pkg dir + mv File.basename(exe_task.name), exe_task.name + end +end + +desc "Build exe" +task "exe:build" => dist("heroku-toolbelt-#{version}.exe") + +desc "Release exe" +task "exe:release" => "exe:build" do |t| + store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-#{version}.exe" + store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-beta.exe" if beta? + store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt.exe" unless beta? +end + +desc "Create wine environment to build windows installer" +task "exe:init-wine" do + setup_wine_env + rm_rf ENV["WINEPREFIX"] + system "wineboot --init" # init wine dir + # replace winemenubuilder with a thing that does nothing, preventing it from poopin' a .config dir into your $HOME + system %q[ + echo "int main(){return 0;}" > noop.c + winegcc noop.c -o noop + mv noop.exe.so "$WINEPREFIX/drive_c/windows/system32/winemenubuilder.exe" + rm noop.* + ] + # set mac wine to use the x11 display driver; iscc borks without this, also it lets us run headless with Xvfb + system %Q[echo '[HKEY_CURRENT_USER\\Software\\Wine\\Drivers]\n"Graphics"="x11"' | regedit -] if $is_mac + # install inno setup + isetup_path = windows_path(cache_file_from_bucket("isetup.exe")).shellescape + system "wine #{isetup_path} /verysilent /suppressmsgboxes /nocancel /norestart /noicons /dir=c:\\inno" + cleanup_after_wine +end + +# Mono's signcode tool can't take the private key passphrase non-interactively (i.e. read file, or as a parameter), so +# in order to run the build non-interactively we have to use a passphrase-less key. To keep the private key secure, the +# key that comes from the repository is encrypted. You can either run exe:build and type in the passphrase manually +# (twice!), or decode it for good with this task. +# +# Ensure your build environment is secure before leaving an unencrypted private key lying around. +# +# Additionally, Mac OS X's default openssl, as of Mavericks, is 0.9.8y, which doesn't support the pvk format. The 1.0.x +# tree does, and you can install it via homebrew (brew install openssl), but it's keg-only, so it'll not be in your +# PATH. You could `brew link` it, but it's safer to leave it alone. Instead, you can pass the full path to the openssl +# binary to be used via the OPENSSL_PATH environment variable: +# +# OPENSSL_PATH=`brew --prefix openssl`/bin/openssl rake exe:pvk-nocrypt +desc "Remove passphrase from heroku-codesign-cert.pvk; see source comments" +task "exe:pvk-nocrypt" do + openssl = (ENV["OPENSSL_PATH"] || "openssl").shellescape + version = `#{openssl} version`.chomp + keyfile_in = resource('exe/heroku-codesign-cert.encrypted.pvk').shellescape + keyfile_out = resource('exe/heroku-codesign-cert.pvk').shellescape + raise "OpenSSL version should be 1.0.x; instead got: #{version}" if version !~ /^OpenSSL 1\./ + system "#{openssl} rsa -inform PVK -outform PVK -pvk-none -in #{keyfile_in} -out #{keyfile_out}" +end + +desc "Link the encrypted pvk" +task "exe:pvk" do + symlink resource("exe/heroku-codesign-cert.encrypted.pvk"), resource("exe/heroku-codesign-cert.pvk") +end From 261e3ee4adde14c9e868ab52d6254a018d262195 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 3 Dec 2014 11:41:28 -0800 Subject: [PATCH 162/952] v3.17.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 934fb1652..752af66af 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.17.0 2014-12-03 +================= +Default to http git +Reduced update check duration to 10 minutes + 3.16.2 2014-11-23 ================= Clean build dist directory before releasing diff --git a/Gemfile.lock b/Gemfile.lock index c34860f1c..700a2f06b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.16.2) + heroku (3.17.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 40ecb90c6..3836f9716 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.16.2" + VERSION = "3.17.0" end From af570bb6ad7509ae01484a7e196b7d382f1061b8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 3 Dec 2014 11:44:28 -0800 Subject: [PATCH 163/952] fixed exe:release --- tasks/exe.rake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tasks/exe.rake b/tasks/exe.rake index b235d1d6c..be1c3cf8f 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -104,9 +104,9 @@ task "exe:build" => dist("heroku-toolbelt-#{version}.exe") desc "Release exe" task "exe:release" => "exe:build" do |t| - store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-#{version}.exe" - store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-beta.exe" if beta? - store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt.exe" unless beta? + s3_store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-#{version}.exe" + s3_store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt-beta.exe" if beta? + s3_store dist("heroku-toolbelt-#{version}.exe"), "heroku-toolbelt/heroku-toolbelt.exe" unless beta? end desc "Create wine environment to build windows installer" From 39b1798683fd8c9424077143a14121c3cf271b12 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 3 Dec 2014 17:56:52 -0800 Subject: [PATCH 164/952] the netrc warning is actually an error --- lib/heroku/command/base.rb | 2 +- lib/heroku/helpers.rb | 4 ++-- spec/spec_helper.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index f549059ea..b34537727 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -264,7 +264,7 @@ def git_url(app_name) if options[:ssh_git] "git@#{Heroku::Auth.git_host}:#{app_name}.git" else - warn_if_netrc_does_not_have_https_git + error_if_netrc_does_not_have_https_git "https://#{Heroku::Auth.http_git_host}/#{app_name}.git" end end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 6945a1843..4ffd708a6 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -528,9 +528,9 @@ def app_owner email org?(email) ? email.gsub(/^(.*)@#{org_host}$/,'\1') : email end - def warn_if_netrc_does_not_have_https_git + def error_if_netrc_does_not_have_https_git unless Auth.netrc && Auth.netrc["git.heroku.com"] - warn "WARNING: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" + warn "ERROR: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" exit 1 end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 60987bd60..f5b98c48a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -203,8 +203,8 @@ module Heroku::Helpers def home_directory @home_directory end - undef_method :warn_if_netrc_does_not_have_https_git - def warn_if_netrc_does_not_have_https_git; end + undef_method :error_if_netrc_does_not_have_https_git + def error_if_netrc_does_not_have_https_git; end end require "support/display_message_matcher" From be131d6d8c07bc3627318e7437eb9fb6947b4020 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 4 Dec 2014 15:20:37 -0800 Subject: [PATCH 165/952] 1.8.7 fixes --- spec/heroku/auth_spec.rb | 6 +++--- tasks/exe.rake | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index edd8e9ec5..e399cbb08 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -128,7 +128,7 @@ module Heroku it "writes credentials and uploads authkey when credentials are saved" do allow(@cli).to receive(:credentials) allow(@cli).to receive(:check) - allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") + allow(@cli).to receive(:ask_for_credentials).and_return(["username", "apikey"]) expect(@cli).to receive(:write_credentials) @cli.ask_for_and_save_credentials end @@ -136,7 +136,7 @@ module Heroku it "save_credentials deletes the credentials when the upload authkey is unauthorized" do allow(@cli).to receive(:write_credentials) allow(@cli).to receive(:retry_login?).and_return(false) - allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") + allow(@cli).to receive(:ask_for_credentials).and_return(["username", "apikey"]) allow(@cli).to receive(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } expect(@cli).to receive(:delete_credentials) expect { @cli.ask_for_and_save_credentials }.to raise_error(SystemExit) @@ -146,7 +146,7 @@ module Heroku allow(@cli).to receive(:read_credentials) allow(@cli).to receive(:write_credentials) allow(@cli).to receive(:delete_credentials) - allow(@cli).to receive(:ask_for_credentials).and_return("username", "apikey") + allow(@cli).to receive(:ask_for_credentials).and_return(["username", "apikey"]) allow(@cli).to receive(:check) { raise Heroku::API::Errors::Unauthorized.new("Login Failed", Excon::Response.new) } expect(@cli).to receive(:ask_for_credentials).exactly(3).times expect { @cli.ask_for_and_save_credentials }.to raise_error(SystemExit) diff --git a/tasks/exe.rake b/tasks/exe.rake index be1c3cf8f..679e08755 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -83,9 +83,9 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| -v "#{windows_path(resource('exe/heroku-codesign-cert.pvk'))}" -a sha1 -$ commercial -n "Heroku Toolbelt" - $f ] # $f gets replaced by iscc with the path to the file it wants to compile - .gsub("\n", ' ') # everything on a single line now - .gsub('"', '$q') # iscc requires quotes to be escaped this way, don't ask + $f ]. # $f gets replaced by iscc with the path to the file it wants to compile + gsub("\n", ' ') # everything on a single line now + gsub('"', '$q') # iscc requires quotes to be escaped this way, don't ask # compile installer under wine! setup_wine_env From 325fa119f473e2e628ab60cc892e93a4932f050c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 4 Dec 2014 15:20:37 -0800 Subject: [PATCH 166/952] added debugging for auth --- lib/heroku/auth.rb | 2 ++ lib/heroku/helpers.rb | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index b240a87ff..38eb1194d 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -13,6 +13,7 @@ class << self def api @api ||= begin + debug "Using API with key: #{password[0,6]}..." api = Heroku::API.new(default_params.merge(:api_key => password)) def api.request(params, &block) @@ -259,6 +260,7 @@ def ask_for_password def ask_for_and_save_credentials @credentials = ask_for_credentials + debug "Logged in as #{@credentials[0]} with key: #{@credentials[1][0,6]}..." write_credentials check @credentials diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 4ffd708a6..23b234179 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -32,6 +32,10 @@ def deprecate(message) display "WARNING: #{message}" end + def debug(*args) + $stderr.puts(*args) if ENV['HEROKU_DEBUG'] + end + def confirm(message="Are you sure you wish to continue? (y/n)") display("#{message} ", false) ['y', 'yes'].include?(ask.downcase) From 64461b2ae88d72090391641d5a1d220dc9e54c23 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 4 Dec 2014 16:14:05 -0800 Subject: [PATCH 167/952] v3.17.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 752af66af..94146633b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.17.1 2014-12-04 +================= +Added debug logging for auth + 3.17.0 2014-12-03 ================= Default to http git diff --git a/Gemfile.lock b/Gemfile.lock index 700a2f06b..2be7e24af 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.17.0) + heroku (3.17.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3836f9716..01f29eeaf 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.17.0" + VERSION = "3.17.1" end From 6ab2222b58d29305bc598b22e62c1fe857693822 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 5 Dec 2014 17:01:27 -0800 Subject: [PATCH 168/952] show warning if HEROKU_API_KEY is set --- lib/heroku/auth.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 38eb1194d..69dbcbcc8 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -267,6 +267,7 @@ def ask_for_and_save_credentials rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e delete_credentials display "Authentication failed." + warn "WARNING: HEROKU_API_KEY is set to an invalid key." if ENV['HEROKU_API_KEY'] retry if retry_login? exit 1 rescue => e From 179311d1a3cb9271bb58983ef801b16b96745492 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 1 Dec 2014 12:29:14 -0800 Subject: [PATCH 169/952] upgraded gems (notably netrc, heroku-api and excon) --- Gemfile.lock | 30 +++++++++++++++--------------- heroku.gemspec | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2be7e24af..6293fc49e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,7 @@ PATH heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) - netrc (~> 0.7.7) + netrc (~> 0.9.0) rest-client (= 1.6.7) rubyzip (= 0.9.9) @@ -18,19 +18,19 @@ GEM mime-types xml-simple builder (3.2.2) - coveralls (0.7.1) + coveralls (0.7.2) multi_json (~> 1.3) - rest-client + rest-client (= 1.6.7) simplecov (>= 0.7) - term-ansicolor - thor + term-ansicolor (= 1.2.2) + thor (= 0.18.1) crack (0.4.2) safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.40.0) - fakefs (0.5.2) - heroku-api (0.3.19) + excon (0.42.0) + fakefs (0.5.4) + heroku-api (0.3.21) excon (~> 0.38) multi_json (~> 1.8) json (1.8.1) @@ -38,8 +38,8 @@ GEM addressable (~> 2.3) mime-types (1.25.1) multi_json (1.10.1) - netrc (0.7.9) - rake (10.3.2) + netrc (0.9.0) + rake (10.4.2) rest-client (1.6.7) mime-types (>= 1.16) rr (1.1.2) @@ -62,11 +62,11 @@ GEM multi_json (~> 1.0) simplecov-html (~> 0.8.0) simplecov-html (0.8.0) - term-ansicolor (1.3.0) - tins (~> 1.0) - thor (0.19.1) - tins (1.3.3) - webmock (1.20.2) + term-ansicolor (1.2.2) + tins (~> 0.8) + thor (0.18.1) + tins (0.13.2) + webmock (1.20.4) addressable (>= 2.3.6) crack (>= 0.3.2) xml-simple (1.1.4) diff --git a/heroku.gemspec b/heroku.gemspec index 37a6109a6..02bd7c838 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.add_dependency "heroku-api", "~> 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" - gem.add_dependency "netrc", "~> 0.7.7" + gem.add_dependency "netrc", "~> 0.9.0" gem.add_dependency "rest-client", "= 1.6.7" gem.add_dependency "rubyzip", "= 0.9.9" gem.add_dependency "multi_json", "~> 1.10.1" From 887cd4518b0919fee01b7b173e29f5a00014edc9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 5 Dec 2014 19:53:35 -0800 Subject: [PATCH 170/952] fixed tests --- spec/heroku/auth_spec.rb | 18 ++++++++++----- spec/heroku/command/apps_spec.rb | 5 ++--- spec/heroku/command/domains_spec.rb | 2 +- spec/heroku/command/fork_spec.rb | 5 ++--- spec/heroku/command/labs_spec.rb | 34 +++++++++++++++-------------- 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index e399cbb08..01d1f2277 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -52,12 +52,14 @@ module Heroku expect(File.exist?(@cli.netrc_path)).to eq(false) # transition - expect(@cli.get_credentials).to eq(['legacy_user', 'legacy_pass']) + expect(@cli.get_credentials.login).to eq('legacy_user') + expect(@cli.get_credentials.password).to eq('legacy_pass') # postconditions expect(File.exist?(@cli.legacy_credentials_path)).to eq(false) expect(File.exist?(@cli.netrc_path)).to eq(true) - expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['legacy_user', 'legacy_pass']) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].login).to eq('legacy_user') + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].password).to eq('legacy_pass') end end @@ -90,7 +92,8 @@ module Heroku @cli.reauthorize end it "updates saved credentials" do - expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['new_user', 'new_password']) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].login).to eq('new_user') + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].password).to eq('new_password') end it "returns environment variable credentials" do expect(@cli.read_credentials).to eq(['', ENV['HEROKU_API_KEY']]) @@ -165,7 +168,8 @@ module Heroku allow(@cli).to receive(:ask_for_credentials).and_return(['one', 'two']) allow(@cli).to receive(:check) @cli.reauthorize - expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"]).to eq(['one', 'two']) + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].login).to eq('one') + expect(Netrc.read(@cli.netrc_path)["api.#{@cli.host}"].password).to eq('two') end it "migrates long api keys to short api keys" do @@ -173,9 +177,11 @@ module Heroku api_key = "7e262de8cac430d8a250793ce8d5b334ae56b4ff15767385121145198a2b4d2e195905ef8bf7cfc5" @cli.netrc["api.#{@cli.host}"] = ["user", api_key] - expect(@cli.get_credentials).to eq(["user", api_key[0,40]]) + expect(@cli.get_credentials.login).to eq("user") + expect(@cli.get_credentials.password).to eq(api_key[0,40]) Auth.subdomains.each do |section| - expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"]).to eq(["user", api_key[0,40]]) + expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"].login).to eq("user") + expect(Netrc.read(@cli.netrc_path)["#{section}.#{@cli.host}"].password).to eq(api_key[0,40]) end end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index e51597fce..250fa95a2 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -123,12 +123,11 @@ module Heroku::Command it "with addons" do with_blank_git_repository do - stderr, stdout = execute("apps:create addonapp --addon custom_domains:basic,releases:basic") + stderr, stdout = execute("apps:create addonapp --addon pgbackups:auto-month") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating addonapp... done, stack is bamboo-mri-1.9.2 -Adding custom_domains:basic to addonapp... done -Adding releases:basic to addonapp... done +Adding pgbackups:auto-month to addonapp... done http://addonapp.herokuapp.com/ | https://git.heroku.com/addonapp.git Git remote heroku added STDOUT diff --git a/spec/heroku/command/domains_spec.rb b/spec/heroku/command/domains_spec.rb index c98cd31ea..c19f78fb2 100644 --- a/spec/heroku/command/domains_spec.rb +++ b/spec/heroku/command/domains_spec.rb @@ -6,7 +6,7 @@ module Heroku::Command before(:all) do api.post_app("name" => "example", "stack" => "cedar") - api.post_addon("example", "custom_domains:basic") + api.post_addon("example", "pgbackups:auto-month") end after(:all) do diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb index 3d2adb050..9cdf0bd21 100644 --- a/spec/heroku/command/fork_spec.rb +++ b/spec/heroku/command/fork_spec.rb @@ -71,10 +71,9 @@ module Heroku::Command end it "re-provisions add-ons" do - addons = ["pgbackups:basic", "deployhooks:http"].sort - addons.each { |a| api.post_addon("example", a) } + api.post_addon("example", "heroku-postgresql:hobby-dev") execute("fork example-fork") - expect(api.get_addons("example-fork").body.collect { |info| info["name"] }.sort).to eq(addons) + expect(api.get_addons("example-fork").body[0]["name"]).to eq("heroku-postgresql:hobby-dev") end end diff --git a/spec/heroku/command/labs_spec.rb b/spec/heroku/command/labs_spec.rb index a0848ece2..a25927934 100644 --- a/spec/heroku/command/labs_spec.rb +++ b/spec/heroku/command/labs_spec.rb @@ -19,11 +19,12 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === User Features (email@example.com) -[ ] sumo-rankings Heroku Sumo ranks and visualizes the scale of your app, and suggests the optimum combination of dynos and add-ons to take it to the next level. +[ ] github-sync Allow users to set up automatic GitHub deployments from Dashboard +[ ] pipelines Pipelines adds experimental support for deploying changes between applications with a shared code base. === App Features (example) -[+] sigterm-all When stopping a dyno, send SIGTERM to all processes rather than only to the root process. -[ ] user_env_compile Add user config vars to the environment during slug compilation +[+] http-dyno-logs Enable HTTP dyno logs using log-shuttle [alpha] +[ ] log-runtime-metrics Emit dyno resource usage information into app logs STDOUT end @@ -33,21 +34,22 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === User Features (email@example.com) -[ ] sumo-rankings Heroku Sumo ranks and visualizes the scale of your app, and suggests the optimum combination of dynos and add-ons to take it to the next level. +[ ] github-sync Allow users to set up automatic GitHub deployments from Dashboard +[ ] pipelines Pipelines adds experimental support for deploying changes between applications with a shared code base. === App Features (example) -[+] sigterm-all When stopping a dyno, send SIGTERM to all processes rather than only to the root process. -[ ] user_env_compile Add user config vars to the environment during slug compilation +[+] http-dyno-logs Enable HTTP dyno logs using log-shuttle [alpha] +[ ] log-runtime-metrics Emit dyno resource usage information into app logs STDOUT end it "displays details of a feature" do - stderr, stdout = execute("labs:info user_env_compile") + stderr, stdout = execute("labs:info pipelines") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -=== user_env_compile -Docs: http://devcenter.heroku.com/articles/labs-user-env-compile -Summary: Add user config vars to the environment during slug compilation +=== pipelines +Docs: https://devcenter.heroku.com/articles/using-pipelines-to-deploy-between-applications +Summary: Pipelines adds experimental support for deploying changes between applications with a shared code base. STDOUT end @@ -61,12 +63,12 @@ module Heroku::Command end it "enables a feature" do - stderr, stdout = execute("labs:enable user_env_compile") + stderr, stdout = execute("labs:enable pipelines") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Enabling user_env_compile for example... done +Enabling pipelines for email@example.com... done WARNING: This feature is experimental and may change or be removed without notice. -For more information see: http://devcenter.heroku.com/articles/labs-user-env-compile +For more information see: https://devcenter.heroku.com/articles/using-pipelines-to-deploy-between-applications STDOUT end @@ -80,11 +82,11 @@ module Heroku::Command end it "disables a feature" do - api.post_feature('user_env_compile', 'example') - stderr, stdout = execute("labs:disable user_env_compile") + api.post_feature('pipelines', 'example') + stderr, stdout = execute("labs:disable pipelines") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Disabling user_env_compile for example... done +Disabling pipelines for email@example.com... done STDOUT end From 4423fe1efbda00e6f3e75189fe97ffc996e4cfb0 Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Sun, 7 Dec 2014 21:50:35 -0800 Subject: [PATCH 171/952] simplify Auth.verify_host? heroku-shadow is gone :} basically never verify if HEROKU_SSL_VERIFY is set, otherwise only verify for production --- lib/heroku/auth.rb | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 69dbcbcc8..f16f89ec7 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -341,10 +341,6 @@ def retry_login? @login_attempts < 3 end - def verified_hosts - %w( heroku.com heroku-shadow.com ) - end - def base_host(host) parts = URI.parse(full_host(host)).host.split(".") return parts.first if parts.size == 1 @@ -356,10 +352,8 @@ def full_host(host) end def verify_host?(host) - hostname = base_host(host) - verified = verified_hosts.include?(hostname) - verified = false if ENV["HEROKU_SSL_VERIFY"] == "disable" - verified + return false if ENV["HEROKU_SSL_VERIFY"] == "disable" + base_host(host) == "heroku.com" end protected From ac5e4c1783e7f03fb094bd0cab9acc1e6e2205db Mon Sep 17 00:00:00 2001 From: Pedro Belo Date: Sun, 7 Dec 2014 21:52:59 -0800 Subject: [PATCH 172/952] change the default manager url to point back to Heroku::Auth this will continue returning api.heroku.com for most cases, but allows us to point the Toolbelt to a different endpoint without having to set HEROKU_MANAGER_URL too. --- lib/heroku/client/organizations.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 8415bebcc..e0452cf81 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -230,7 +230,7 @@ def decompress_response!(response) end def manager_url - ENV['HEROKU_MANAGER_URL'] || "https://api.heroku.com" + ENV['HEROKU_MANAGER_URL'] || Heroku::Auth.full_host(Heroku::Auth.host) end end From a40e3d2d5bd02a7a720e185c2cf9d0ea7e40d9d4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 8 Dec 2014 11:03:30 -0800 Subject: [PATCH 173/952] v3.18.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 94146633b..dd67653cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.18.0 2014-12-05 +================= +Upgraded gems (notably netrc, heroku-api and excon) +Show warning if HEROKU_API_KEY is set +Tweaks to manager url + 3.17.1 2014-12-04 ================= Added debug logging for auth diff --git a/Gemfile.lock b/Gemfile.lock index 6293fc49e..d34f18376 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.17.1) + heroku (3.18.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 01f29eeaf..7ec10a5a6 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.17.1" + VERSION = "3.18.0" end From 009cc7e9b155b854c0b37e2d9134de12944339d4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 8 Dec 2014 11:11:47 -0800 Subject: [PATCH 174/952] exe rake task fixes --- tasks/exe.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/exe.rake b/tasks/exe.rake index 679e08755..c9ced09f6 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -58,7 +58,7 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| installer_path = "#{build_path}/heroku-installer" heroku_cli_path = "#{installer_path}/heroku" mkdir_p heroku_cli_path - extract_zip "#{$base_path}/dist/heroku-3.16.2.zip", "#{heroku_cli_path}/" + extract_zip "#{$base_path}/dist/heroku-#{version}.zip", "#{heroku_cli_path}/" # gather the ruby and git installers, downlading from s3 mkdir "#{installer_path}/installers" @@ -84,7 +84,7 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| -a sha1 -$ commercial -n "Heroku Toolbelt" $f ]. # $f gets replaced by iscc with the path to the file it wants to compile - gsub("\n", ' ') # everything on a single line now + gsub("\n", ' '). # everything on a single line now gsub('"', '$q') # iscc requires quotes to be escaped this way, don't ask # compile installer under wine! From c8ed2f058995960eeae18f2073f6e6a01c82cb95 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 8 Dec 2014 14:38:36 -0800 Subject: [PATCH 175/952] use $0 for binary name --- lib/heroku/updater.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 04aa68483..3fe2b3438 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -172,10 +172,10 @@ def self.background_update! FileUtils.mkdir_p File.dirname(log_path) pid = if defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/ fork do - exec("heroku update &> #{log_path} 2>&1") + exec("#{$0} update &> #{log_path} 2>&1") end else - spawn("heroku update", {:err => log_path, :out => log_path}) + spawn("#{$0} update", {:err => log_path, :out => log_path}) end Process.detach(pid) FileUtils.mkdir_p File.dirname(last_autoupdate_path) From cbbf935e00f8c21502c6ba86b983f88f8acc0b50 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 8 Dec 2014 09:55:17 -0800 Subject: [PATCH 176/952] OSX package rake task --- resources/pkg/Distribution.erb | 32 ++++++++++++++++ resources/pkg/PackageInfo.erb | 6 +++ resources/pkg/has_git | 2 + resources/pkg/heroku | 24 ++++++++++++ resources/pkg/postinstall | 44 +++++++++++++++++++++ tasks/pkg.rake | 70 ++++++++++++++++++++++++++++++++++ 6 files changed, 178 insertions(+) create mode 100644 resources/pkg/Distribution.erb create mode 100644 resources/pkg/PackageInfo.erb create mode 100755 resources/pkg/has_git create mode 100755 resources/pkg/heroku create mode 100755 resources/pkg/postinstall create mode 100644 tasks/pkg.rake diff --git a/resources/pkg/Distribution.erb b/resources/pkg/Distribution.erb new file mode 100644 index 000000000..e8cab8ffd --- /dev/null +++ b/resources/pkg/Distribution.erb @@ -0,0 +1,32 @@ + + + Heroku Toolbelt + + + + + + + + + + + + + + + + + + + + #foreman.pkg + #git-etc.pkg + #git-git.pkg + #heroku-client.pkg + #ruby.pkg + diff --git a/resources/pkg/PackageInfo.erb b/resources/pkg/PackageInfo.erb new file mode 100644 index 000000000..5a0da8998 --- /dev/null +++ b/resources/pkg/PackageInfo.erb @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/pkg/has_git b/resources/pkg/has_git new file mode 100755 index 000000000..28e94beef --- /dev/null +++ b/resources/pkg/has_git @@ -0,0 +1,2 @@ +#!/bin/sh +which git >/dev/null diff --git a/resources/pkg/heroku b/resources/pkg/heroku new file mode 100755 index 000000000..710d628d9 --- /dev/null +++ b/resources/pkg/heroku @@ -0,0 +1,24 @@ +#!/usr/local/heroku/ruby/bin/ruby +# encoding: UTF-8 + +# resolve bin path, ignoring symlinks +require "pathname" +bin_file = Pathname.new(__FILE__).realpath + +# add locally vendored gems to libpath +gem_dir = File.expand_path("../../vendor/gems", bin_file) +Dir["#{gem_dir}/**/lib"].each do |libdir| + $:.unshift libdir +end + +# add self to libpath +$:.unshift File.expand_path("../../lib", bin_file) + +# inject any code in ~/.heroku/client over top +require "heroku/updater" +Heroku::Updater.inject_libpath + +# start up the CLI +require "heroku/cli" +Heroku.user_agent = "heroku-toolbelt/#{Heroku::VERSION} (#{RUBY_PLATFORM}) ruby/#{RUBY_VERSION}" +Heroku::CLI.start(*ARGV) diff --git a/resources/pkg/postinstall b/resources/pkg/postinstall new file mode 100755 index 000000000..c072c83f3 --- /dev/null +++ b/resources/pkg/postinstall @@ -0,0 +1,44 @@ +#!/bin/sh + +usershell=$(dscl localhost -read /Local/Default/Users/$USER shell | sed -e 's/[^ ]* //') + +startup_files() { + case $(basename $usershell) in + zsh) + echo ".zlogin .zshrc .zprofile .zshenv" + ;; + bash) + echo ".bashrc .bash_profile .bash_login .profile" + ;; + *) + echo ".bash_profile .zshrc .profile" + ;; + esac +} + +install_path() { + for file in $(startup_files); do + [ -f $HOME/$file ] || continue + (grep "Added by the Heroku" $HOME/$file >/dev/null) && break + + cat <>$HOME/$file +### Added by the Heroku Toolbelt +export PATH="/usr/local/heroku/bin:\$PATH" +MESSAGE + + # done after we add to one file + break + done +} + +# if the toolbelt is not returned by `which`, let's add to the PATH +case $(which heroku) in + /usr/bin/heroku|/usr/local/heroku/bin/heroku) + ;; + *) + install_path + ;; +esac + +# symlink binary to /usr/bin/heroku +ln -sf /usr/local/heroku/bin/heroku /usr/bin/heroku diff --git a/tasks/pkg.rake b/tasks/pkg.rake new file mode 100644 index 000000000..74e52efaf --- /dev/null +++ b/tasks/pkg.rake @@ -0,0 +1,70 @@ +file dist("heroku-#{version}.pkg") => distribution_files("pkg") do |t| + tempdir do |dir| + mkdir "heroku-client" + cd "heroku-client" do + assemble_distribution + assemble_gems + assemble resource("pkg/heroku"), "bin/heroku", 0755 + end + + mkdir_p "pkg" + mkdir_p "pkg/Resources" + mkdir_p "pkg/heroku-client.pkg" + + kbytes = %x{ du -ks pkg | cut -f 1 } + num_files = %x{ find pkg | wc -l } + + dist = File.read(resource("pkg/Distribution.erb")) + dist = ERB.new(dist).result(binding) + File.open("pkg/Distribution", "w") { |f| f.puts dist } + + dist = File.read(resource("pkg/PackageInfo.erb")) + dist = ERB.new(dist).result(binding) + File.open("pkg/heroku-client.pkg/PackageInfo", "w") { |f| f.puts dist } + + mkdir_p "pkg/Scripts" + cp resource("pkg/has_git"), "pkg/Scripts/has_git" + + mkdir_p "pkg/heroku-client.pkg/Scripts" + cp resource("pkg/postinstall"), "pkg/heroku-client.pkg/Scripts/postinstall" + chmod 0755, "pkg/heroku-client.pkg/Scripts/postinstall" + + sh %{ mkbom -s heroku-client pkg/heroku-client.pkg/Bom } + + Dir.chdir("heroku-client") do + sh %{ find . | cpio -o --format odc | gzip -c > ../pkg/heroku-client.pkg/Payload } + end + + unless File.exists?(dist('foreman-0.75.0.pkg')) + sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/foreman-0.75.0.pkg -o #{dist('foreman-0.75.0.pkg')} } + end + sh %{ pkgutil --expand #{dist('foreman-0.75.0.pkg')} foreman } + mv "foreman/foreman.pkg", "pkg/foreman.pkg" + + unless File.exists?(dist('ruby.pkg')) + sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o #{dist('ruby.pkg')} } + end + sh %{ pkgutil --expand #{dist('ruby.pkg')} ruby } + mv "ruby/ruby-1.9.3-p194.pkg", "pkg/ruby.pkg" + + unless File.exists?(dist('git.pkg')) + sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/git.pkg -o #{dist('git.pkg')} } + end + sh %{ pkgutil --expand #{dist('git.pkg')} git } + mv "git/etc.pkg", "pkg/git-etc.pkg" + mv "git/git.pkg", "pkg/git-git.pkg" + + sh %{ pkgutil --flatten pkg heroku-toolbelt-#{version}.pkg } + sh %{ productsign --sign "Developer ID Installer: Heroku INC" heroku-toolbelt-#{version}.pkg heroku-toolbelt-#{version}-signed.pkg } + cp_r "heroku-toolbelt-#{version}-signed.pkg", t.name + end +end + +desc "build pkg" +task "pkg:build" => dist("heroku-#{version}.pkg") + +task "pkg:release" do + s3_store pkg("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" + s3_store pkg("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-beta.pkg" if beta? + s3_store pkg("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt.pkg" unless beta? +end From e5ea93cbcd914809fb63d7d029ab6e1355ad947b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 9 Dec 2014 11:34:41 -0800 Subject: [PATCH 177/952] fixed pkg:release --- tasks/pkg.rake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tasks/pkg.rake b/tasks/pkg.rake index 74e52efaf..ca42c57f3 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -64,7 +64,7 @@ desc "build pkg" task "pkg:build" => dist("heroku-#{version}.pkg") task "pkg:release" do - s3_store pkg("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" - s3_store pkg("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-beta.pkg" if beta? - s3_store pkg("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt.pkg" unless beta? + s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" + s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-beta.pkg" if beta? + s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt.pkg" unless beta? end From 8f4d84928132749814d0bcbaca0715ad6878a288 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 9 Dec 2014 17:10:25 -0800 Subject: [PATCH 178/952] build package before releasing --- tasks/pkg.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/pkg.rake b/tasks/pkg.rake index ca42c57f3..766adcfbc 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -63,7 +63,7 @@ end desc "build pkg" task "pkg:build" => dist("heroku-#{version}.pkg") -task "pkg:release" do +task "pkg:release" => dist("heroku-toolbelt-#{version}.pkg") do s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-beta.pkg" if beta? s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt.pkg" unless beta? From 2485df0537ce0dcddc3925f5ec61ad2600931881 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 9 Dec 2014 17:09:35 -0800 Subject: [PATCH 179/952] update inline --- lib/heroku/cli.rb | 39 ++++++++++++++++++------------------ lib/heroku/command/update.rb | 11 ++-------- lib/heroku/updater.rb | 35 ++++++++++++-------------------- tasks/pkg.rake | 4 ++-- 4 files changed, 36 insertions(+), 53 deletions(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 15f98b8ad..623d12e2d 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -28,27 +28,26 @@ class Heroku::CLI extend Heroku::Helpers def self.start(*args) - begin - if $stdin.isatty - $stdin.sync = true - end - if $stdout.isatty - $stdout.sync = true - end - command = args.shift.strip rescue "help" - Heroku::Command.load - Heroku::Command.run(command, args) - rescue Interrupt => e - `stty icanon echo` - if ENV["HEROKU_DEBUG"] - styled_error(e) - else - error("Command cancelled.") - end - rescue => error - styled_error(error) - exit(1) + if $stdin.isatty + $stdin.sync = true end + if $stdout.isatty + $stdout.sync = true + end + command = args.shift.strip rescue "help" + Heroku::Command.load + Heroku::Command.run(command, args) + Heroku::Updater.autoupdate + rescue Interrupt => e + `stty icanon echo` + if ENV["HEROKU_DEBUG"] + styled_error(e) + else + error("Command cancelled.") + end + rescue => error + styled_error(error) + exit(1) end end diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 7baeb230e..377339b35 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -31,17 +31,10 @@ def beta update_from_url(true) end -private + private def update_from_url(prerelease) Heroku::Updater.check_disabled! - action("Updating") do - if new_version = Heroku::Updater.update(prerelease) - status("#{Heroku::VERSION} updated to #{new_version}") - else - status("nothing to update") - end - end + Heroku::Updater.update(prerelease) end - end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 3fe2b3438..2398e0c5a 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -85,9 +85,20 @@ def self.wait_for_lock(wait_for=5, check_every=0.5) FileUtils.rm_f path end - def self.update(prerelease) + def self.autoupdate + # if we've updated in the last hour, don't try again + if File.exists?(last_autoupdate_path) + return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60 + end + FileUtils.mkdir_p File.dirname(last_autoupdate_path) + FileUtils.touch last_autoupdate_path + update + end + + def self.update(prerelease=false) return unless prerelease || needs_update? + $stderr.print 'updating...' wait_for_lock do require "tmpdir" require "zip/zip" @@ -118,6 +129,7 @@ def self.update(prerelease) FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path + $stderr.puts "done. Updated to #{version}" version end end @@ -155,31 +167,10 @@ def self.inject_libpath end load('heroku/updater.rb') # reload updated updater end - - background_update! end def self.last_autoupdate_path File.join(Heroku::Helpers.home_directory, ".heroku", "autoupdate.last") end - - def self.background_update! - # if we've updated in the last 10 minutes, don't try again - if File.exists?(last_autoupdate_path) - return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*10 - end - log_path = File.join(Heroku::Helpers.home_directory, '.heroku', 'autoupdate.log') - FileUtils.mkdir_p File.dirname(log_path) - pid = if defined?(RUBY_VERSION) and RUBY_VERSION =~ /^1\.8\.\d+/ - fork do - exec("#{$0} update &> #{log_path} 2>&1") - end - else - spawn("#{$0} update", {:err => log_path, :out => log_path}) - end - Process.detach(pid) - FileUtils.mkdir_p File.dirname(last_autoupdate_path) - FileUtils.touch last_autoupdate_path - end end end diff --git a/tasks/pkg.rake b/tasks/pkg.rake index 766adcfbc..ba7034be0 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -1,4 +1,4 @@ -file dist("heroku-#{version}.pkg") => distribution_files("pkg") do |t| +file dist("heroku-toolbelt-#{version}.pkg") => distribution_files("pkg") do |t| tempdir do |dir| mkdir "heroku-client" cd "heroku-client" do @@ -61,7 +61,7 @@ file dist("heroku-#{version}.pkg") => distribution_files("pkg") do |t| end desc "build pkg" -task "pkg:build" => dist("heroku-#{version}.pkg") +task "pkg:build" => dist("heroku-toolbelt-#{version}.pkg") task "pkg:release" => dist("heroku-toolbelt-#{version}.pkg") do s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" From cc859fc6446024194958d1ee43c19a49435996b4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 9 Dec 2014 17:28:30 -0800 Subject: [PATCH 180/952] v3.19.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd67653cb..7a8cd9eaf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.19.0 2014-12-09 +================= +Simplified updating by performing updates synchonously instead of in a separate process + 3.18.0 2014-12-05 ================= Upgraded gems (notably netrc, heroku-api and excon) diff --git a/Gemfile.lock b/Gemfile.lock index d34f18376..9d2a98ac9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.18.0) + heroku (3.19.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 7ec10a5a6..e09a0847c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.18.0" + VERSION = "3.19.0" end From ececdabc909c87838bac3d347d6fccef0f0aa929 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 10 Dec 2014 12:05:22 -0800 Subject: [PATCH 181/952] use Dir.home for ruby 1.9+ --- lib/heroku/helpers.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 23b234179..e358ea78c 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -4,6 +4,7 @@ module Helpers extend self def home_directory + return Dir.home if defined? Dir.home # Ruby 1.9+ running_on_windows? ? ENV['USERPROFILE'].gsub("\\","/") : ENV['HOME'] end From afc0a919c4a990df5a60ed318dd8a2b438a23516 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 10 Dec 2014 12:42:14 -0800 Subject: [PATCH 182/952] upgraded excon and netrc gems --- Gemfile.lock | 6 +++--- heroku.gemspec | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9d2a98ac9..3b40b3f82 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,7 +5,7 @@ PATH heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) - netrc (~> 0.9.0) + netrc (>= 0.10.0) rest-client (= 1.6.7) rubyzip (= 0.9.9) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.42.0) + excon (0.42.1) fakefs (0.5.4) heroku-api (0.3.21) excon (~> 0.38) @@ -38,7 +38,7 @@ GEM addressable (~> 2.3) mime-types (1.25.1) multi_json (1.10.1) - netrc (0.9.0) + netrc (0.10.0) rake (10.4.2) rest-client (1.6.7) mime-types (>= 1.16) diff --git a/heroku.gemspec b/heroku.gemspec index 02bd7c838..964869671 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -22,7 +22,7 @@ Gem::Specification.new do |gem| gem.add_dependency "heroku-api", "~> 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" - gem.add_dependency "netrc", "~> 0.9.0" + gem.add_dependency "netrc", ">= 0.10.0" gem.add_dependency "rest-client", "= 1.6.7" gem.add_dependency "rubyzip", "= 0.9.9" gem.add_dependency "multi_json", "~> 1.10.1" From 59f9f3070654e1a2bb175243b7ba9ae0e5baf5f3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 10 Dec 2014 12:51:26 -0800 Subject: [PATCH 183/952] show plugins in heroku version --- lib/heroku/command/version.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/version.rb b/lib/heroku/command/version.rb index e87e320c8..fe2a90626 100644 --- a/lib/heroku/command/version.rb +++ b/lib/heroku/command/version.rb @@ -18,6 +18,11 @@ def index validate_arguments! display(Heroku.user_agent) - end + plugins = Heroku::Plugin.list + if plugins.length > 0 + styled_header("Installed Plugins") + styled_array(plugins) + end + end end From a658050440cd9d074c6df78d69bb72513453fd62 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 10 Dec 2014 12:56:31 -0800 Subject: [PATCH 184/952] v3.20.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7a8cd9eaf..a1e95262f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.20.0 2014-12-10 +================= +Upgraded excon and netrc gems +Use Dir.home for Ruby 1.9+ +Show plugins in `heroku version` + 3.19.0 2014-12-09 ================= Simplified updating by performing updates synchonously instead of in a separate process diff --git a/Gemfile.lock b/Gemfile.lock index 3b40b3f82..8afafbad9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.19.0) + heroku (3.20.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e09a0847c..31b774374 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.19.0" + VERSION = "3.20.0" end From bc75c8bcd9286d696d67b12c17e3778fda868746 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 11 Dec 2014 17:15:46 -0800 Subject: [PATCH 185/952] show warning when using heroku-accounts --- lib/heroku/auth.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index f16f89ec7..713b8ab09 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -259,6 +259,7 @@ def ask_for_password end def ask_for_and_save_credentials + warn "WARNING: heroku-accounts plugin is installed. This plugin is known to have problems with HTTP Git." if defined?(Heroku::Command::Accounts) @credentials = ask_for_credentials debug "Logged in as #{@credentials[0]} with key: #{@credentials[1][0,6]}..." write_credentials From 71478320d025d67b81d808f5e77f15fedb415442 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 16 Dec 2014 16:30:22 -0800 Subject: [PATCH 186/952] http instrumentor --- lib/heroku/auth.rb | 6 ++++- lib/heroku/cli.rb | 1 + lib/heroku/helpers.rb | 6 ++++- lib/heroku/http_instrumentor.rb | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 lib/heroku/http_instrumentor.rb diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 713b8ab09..ef18bae37 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -211,6 +211,7 @@ def ask_for_credentials print "Password (typing will be hidden): " password = running_on_windows? ? ask_for_password_on_windows : ask_for_password + HTTPInstrumentor.filter_parameter(password) [user, api_key(user, password)] end @@ -361,13 +362,16 @@ def verify_host?(host) def default_params uri = URI.parse(full_host(host)) - { + params = { :headers => {'User-Agent' => Heroku.user_agent}, :host => uri.host, :port => uri.port.to_s, :scheme => uri.scheme, :ssl_verify_peer => verify_host?(host) } + params[:instrumentor] = HTTPInstrumentor if debugging? + + params end end end diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 623d12e2d..3d61b64fc 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -11,6 +11,7 @@ require "heroku" require "heroku/command" require "heroku/helpers" +require "heroku/http_instrumentor" require 'rest_client' require 'heroku-api' diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index e358ea78c..48dd179ba 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -34,7 +34,11 @@ def deprecate(message) end def debug(*args) - $stderr.puts(*args) if ENV['HEROKU_DEBUG'] + $stderr.puts(*args) if debugging? + end + + def debugging? + ENV['HEROKU_DEBUG'] end def confirm(message="Are you sure you wish to continue? (y/n)") diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb new file mode 100644 index 000000000..884d65b0c --- /dev/null +++ b/lib/heroku/http_instrumentor.rb @@ -0,0 +1,45 @@ +class HTTPInstrumentor + class << self + def filter_parameter(parameter) + @filter_parameters ||= [] + @filter_parameters << parameter + end + + def instrument(name, params={}, &block) + headers = params[:headers] + case name + when "excon.error" + $stderr.puts params[:error].message + when "excon.request" + $stderr.print "HTTP #{params[:method].upcase} #{params[:scheme]}://#{params[:host]}#{params[:path]} " + $stderr.print "[auth] " if headers['Authorization'] && headers['Authorization'] != 'Basic Og==' + $stderr.print "[2fa] " if headers['Heroku-Two-Factor-Code'] + $stderr.puts filter(params[:query]) + when "excon.response" + $stderr.puts "#{params[:status]} #{params[:reason_phrase]}" + if headers['Content-Encoding'] == 'gzip' + $stderr.puts filter(ungzip(params[:body])) + else + $stderr.puts filter(params[:body]) + end + else + $stderr.puts name + end + yield if block_given? + end + + private + + def ungzip(string) + Zlib::GzipReader.new(StringIO.new(string)).read() + end + + def filter(obj) + string = obj.to_s + @filter_parameters.each do |parameter| + string.gsub! parameter, '[FILTERED]' + end + string + end + end +end From 7351f4406f46063eddc36ad1ba05afa5c7b26251 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 17 Dec 2014 11:02:14 -0800 Subject: [PATCH 187/952] dont fail if no filter parameters --- lib/heroku/http_instrumentor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb index 884d65b0c..5dd364f4f 100644 --- a/lib/heroku/http_instrumentor.rb +++ b/lib/heroku/http_instrumentor.rb @@ -36,7 +36,7 @@ def ungzip(string) def filter(obj) string = obj.to_s - @filter_parameters.each do |parameter| + (@filter_parameters || []).each do |parameter| string.gsub! parameter, '[FILTERED]' end string From c03904c72ca4a8b08d3525571f4dac26cb1aaf7c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 16 Dec 2014 16:30:22 -0800 Subject: [PATCH 188/952] use 2fa directly instead of auto-preauth strategy --- Gemfile.lock | 2 +- lib/heroku/auth.rb | 16 +++++++--------- lib/heroku/command.rb | 10 +++++++++- lib/heroku/command/base.rb | 4 ++++ lib/heroku/command/config.rb | 1 + lib/heroku/command/pg.rb | 16 ++++++++++++++++ 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8afafbad9..ec172b7f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,7 +30,7 @@ GEM docile (1.1.5) excon (0.42.1) fakefs (0.5.4) - heroku-api (0.3.21) + heroku-api (0.3.22) excon (~> 0.38) multi_json (~> 1.8) json (1.8.1) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index ef18bae37..bbe30408a 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -82,16 +82,14 @@ def password # :nodoc: get_credentials[1] end - def api_key(user=get_credentials[0], password=get_credentials[1], second_factor=nil) - params = default_params - if second_factor - params[:headers].merge!("Heroku-Two-Factor-Code" => second_factor) - end - api = Heroku::API.new(params) - api.post_login(user, password).body["api_key"] + def api_key(user=get_credentials[0], password=get_credentials[1]) + @api ||= Heroku::API.new(default_params) + api_key = @api.post_login(user, password).body["api_key"] + @api = nil + api_key rescue Heroku::API::Errors::Forbidden => e if e.response.headers.has_key?("Heroku-Two-Factor-Required") - second_factor = ask_for_second_factor + ask_for_second_factor retry end rescue Heroku::API::Errors::Unauthorized => e @@ -218,7 +216,7 @@ def ask_for_credentials def ask_for_second_factor $stderr.print "Two-factor code: " - ask + api.second_factor = ask end def preauth diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 73cebc495..24a62b625 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -9,6 +9,10 @@ class CommandFailed < RuntimeError; end extend Heroku::Helpers + class << self + attr_accessor :requires_preauth + end + def self.load Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file| require file @@ -244,7 +248,11 @@ def self.run(cmd, arguments=[]) error "API request timed out. Please try again, or contact support@heroku.com if this issue persists." rescue Heroku::API::Errors::Forbidden => e if e.response.headers.has_key?("Heroku-Two-Factor-Required") - Heroku::Auth.preauth + if requires_preauth + Heroku::Auth.preauth + else + Heroku::Auth.ask_for_second_factor + end retry else error extract_error(e.response.body) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index b34537727..d10df52f1 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -294,6 +294,10 @@ def git_remotes(base_dir=Dir.pwd) def escape(value) heroku.escape(value) end + + def requires_preauth + Heroku::Command.requires_preauth = true + end end module Heroku::Command diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index cce5185b8..86dfcc223 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -65,6 +65,7 @@ def index # B: two # def set + requires_preauth unless args.size > 0 and args.all? { |a| a.include?('=') } error("Usage: heroku config:set KEY1=VALUE1 [KEY2=VALUE2 ...]\nMust specify KEY and VALUE to set.") end diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 69c46bb47..63c4ac752 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -25,6 +25,7 @@ def set_commands(shorthand) # list databases for an app # def index + requires_preauth validate_arguments! if hpg_databases_with_info.empty? @@ -47,6 +48,7 @@ def index def info db = shift_argument validate_arguments! + requires_preauth if db @resolver = generate_resolver @@ -64,6 +66,7 @@ def info # defaults to DATABASE_URL databases if no DATABASE is specified # if REPORT_ID is specified instead, a previous report is displayed def diagnose + requires_preauth db_id = shift_argument run_diagnose(db_id) end @@ -73,6 +76,7 @@ def diagnose # sets DATABASE as your DATABASE_URL # def promote + requires_preauth unless db = shift_argument error("Usage: heroku pg:promote DATABASE\nMust specify DATABASE to promote.") end @@ -94,6 +98,7 @@ def promote # defaults to DATABASE_URL databases if no DATABASE is specified # def psql + requires_preauth attachment = generate_resolver.resolve(shift_argument, "DATABASE_URL") validate_arguments! @@ -123,6 +128,7 @@ def psql # delete all data in DATABASE # def reset + requires_preauth unless db = shift_argument error("Usage: heroku pg:reset DATABASE\nMust specify DATABASE to reset.") end @@ -144,6 +150,7 @@ def reset # stop a replica from following and make it a read/write database # def unfollow + requires_preauth unless db = shift_argument error("Usage: heroku pg:unfollow REPLICA\nMust specify REPLICA to unfollow.") end @@ -177,6 +184,7 @@ def unfollow # defaults to all databases if no DATABASE is specified # def wait + requires_preauth db = shift_argument validate_arguments! @@ -196,6 +204,7 @@ def wait # --reset # Reset credentials on the specified database. # def credentials + requires_preauth unless db = shift_argument error("Usage: heroku pg:credentials DATABASE\nMust specify DATABASE to display credentials.") end @@ -228,6 +237,7 @@ def credentials # view active queries with execution time # def ps + requires_preauth sql = %Q( SELECT #{pid_column}, @@ -260,6 +270,7 @@ def ps # -f,--force # terminates the connection in addition to cancelling the query # def kill + requires_preauth procpid = shift_argument output_with_bang "procpid to kill is required" unless procpid && procpid.to_i != 0 procpid = procpid.to_i @@ -275,6 +286,7 @@ def kill # terminates ALL connections # def killall + requires_preauth db = args.first attachment = generate_resolver.resolve(db, "DATABASE_URL") client = hpg_client(attachment) @@ -299,6 +311,7 @@ def killall # push from LOCAL_SOURCE_DATABASE to REMOTE_TARGET_DATABASE # REMOTE_TARGET_DATABASE must be empty. def push + requires_preauth local, remote = shift_argument, shift_argument unless [remote, local].all? Heroku::Command.run(current_command, ['--help']) @@ -324,6 +337,7 @@ def push # pull from REMOTE_SOURCE_DATABASE to LOCAL_TARGET_DATABASE # LOCAL_TARGET_DATABASE must not already exist. def pull + requires_preauth remote, local = shift_argument, shift_argument unless [remote, local].all? Heroku::Command.run(current_command, ['--help']) @@ -354,6 +368,7 @@ def pull # window="" # set weekly UTC maintenance window for DATABASE # # eg: `heroku pg:maintenance window="Sunday 14:30"` def maintenance + requires_preauth mode_with_argument = shift_argument || '' mode, mode_argument = mode_with_argument.split('=') @@ -397,6 +412,7 @@ def maintenance # unfollow a database and upgrade it to the latest PostgreSQL version # def upgrade + requires_preauth unless db = shift_argument error("Usage: heroku pg:upgrade REPLICA\nMust specify REPLICA to upgrade.") end From ad8fc041cddba20b80a35410c6fee4080b874f76 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 17 Dec 2014 13:20:26 -0800 Subject: [PATCH 189/952] v3.21.0 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a1e95262f..b447b5ce5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.21.0 2014-12-17 +================= +Upgraded heroku-api gem +Explicitly preauth for 2fa commands instead of automatically on every failure +Show warning when using heroku-accounts (since it is incompatible with http-git) +Added HTTP instrumentor for debugging with HEROKU_DEBUG=true + 3.20.0 2014-12-10 ================= Upgraded excon and netrc gems diff --git a/Gemfile.lock b/Gemfile.lock index ec172b7f4..519f45411 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.20.0) + heroku (3.21.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 31b774374..14ec490a9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.20.0" + VERSION = "3.21.0" end From 5ab8d659746b2dc767b36e74c947f3fb59c010cb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 17 Dec 2014 13:48:34 -0800 Subject: [PATCH 190/952] v3.21.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b447b5ce5..37a1d7bc9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.21.1 2014-12-17 +================= +No changes, needed to bump the version + 3.21.0 2014-12-17 ================= Upgraded heroku-api gem diff --git a/Gemfile.lock b/Gemfile.lock index 519f45411..a9549c6f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.21.0) + heroku (3.21.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 14ec490a9..aeb3a26de 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.21.0" + VERSION = "3.21.1" end From 607e125dc25f1e7c81bc7652350e40b0405db0f6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 18 Dec 2014 16:52:47 -0800 Subject: [PATCH 191/952] removed git from pkg install --- resources/pkg/Distribution.erb | 12 ------------ resources/pkg/has_git | 2 -- tasks/pkg.rake | 8 -------- 3 files changed, 22 deletions(-) delete mode 100755 resources/pkg/has_git diff --git a/resources/pkg/Distribution.erb b/resources/pkg/Distribution.erb index e8cab8ffd..43638a952 100644 --- a/resources/pkg/Distribution.erb +++ b/resources/pkg/Distribution.erb @@ -3,30 +3,18 @@ Heroku Toolbelt - - - - - - #foreman.pkg - #git-etc.pkg - #git-git.pkg #heroku-client.pkg #ruby.pkg diff --git a/resources/pkg/has_git b/resources/pkg/has_git deleted file mode 100755 index 28e94beef..000000000 --- a/resources/pkg/has_git +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -which git >/dev/null diff --git a/tasks/pkg.rake b/tasks/pkg.rake index ba7034be0..5b6e4a4d2 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -23,7 +23,6 @@ file dist("heroku-toolbelt-#{version}.pkg") => distribution_files("pkg") do |t| File.open("pkg/heroku-client.pkg/PackageInfo", "w") { |f| f.puts dist } mkdir_p "pkg/Scripts" - cp resource("pkg/has_git"), "pkg/Scripts/has_git" mkdir_p "pkg/heroku-client.pkg/Scripts" cp resource("pkg/postinstall"), "pkg/heroku-client.pkg/Scripts/postinstall" @@ -47,13 +46,6 @@ file dist("heroku-toolbelt-#{version}.pkg") => distribution_files("pkg") do |t| sh %{ pkgutil --expand #{dist('ruby.pkg')} ruby } mv "ruby/ruby-1.9.3-p194.pkg", "pkg/ruby.pkg" - unless File.exists?(dist('git.pkg')) - sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/git.pkg -o #{dist('git.pkg')} } - end - sh %{ pkgutil --expand #{dist('git.pkg')} git } - mv "git/etc.pkg", "pkg/git-etc.pkg" - mv "git/git.pkg", "pkg/git-git.pkg" - sh %{ pkgutil --flatten pkg heroku-toolbelt-#{version}.pkg } sh %{ productsign --sign "Developer ID Installer: Heroku INC" heroku-toolbelt-#{version}.pkg heroku-toolbelt-#{version}-signed.pkg } cp_r "heroku-toolbelt-#{version}-signed.pkg", t.name From 4b8ccb446fd0fbba271caa7e62b18cf50710a6e3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 18 Dec 2014 17:29:57 -0800 Subject: [PATCH 192/952] move cache to dist/cache --- tasks/exe.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/exe.rake b/tasks/exe.rake index c9ced09f6..70bebf3a9 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -3,7 +3,7 @@ require "shellwords" $is_mac = RUBY_PLATFORM =~ /darwin/ $base_path = File.expand_path(File.join(File.dirname(__FILE__), "..")) -$cache_path = File.join($base_path, ".cache") +$cache_path = File.join($base_path, "dist", "cache") def windows_path(path); `winepath -w #{path.shellescape}`.chomp; end def setup_wine_env From f8f9ef345c546817891d98f31a4f3ecd59e0c4d2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 19 Dec 2014 11:43:05 -0800 Subject: [PATCH 193/952] use staging host for http git netrc check --- lib/heroku/command/base.rb | 5 ++++- lib/heroku/helpers.rb | 7 ++----- spec/spec_helper.rb | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index d10df52f1..ea85d9094 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -264,7 +264,10 @@ def git_url(app_name) if options[:ssh_git] "git@#{Heroku::Auth.git_host}:#{app_name}.git" else - error_if_netrc_does_not_have_https_git + unless has_http_git_entry_in_netrc + warn "ERROR: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" + exit 1 + end "https://#{Heroku::Auth.http_git_host}/#{app_name}.git" end end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 48dd179ba..8047b5623 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -537,11 +537,8 @@ def app_owner email org?(email) ? email.gsub(/^(.*)@#{org_host}$/,'\1') : email end - def error_if_netrc_does_not_have_https_git - unless Auth.netrc && Auth.netrc["git.heroku.com"] - warn "ERROR: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" - exit 1 - end + def has_http_git_entry_in_netrc + Auth.netrc && Auth.netrc[Auth.http_git_host] end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f5b98c48a..5f1a01f39 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -203,8 +203,10 @@ module Heroku::Helpers def home_directory @home_directory end - undef_method :error_if_netrc_does_not_have_https_git - def error_if_netrc_does_not_have_https_git; end + undef_method :has_http_git_entry_in_netrc + def has_http_git_entry_in_netrc + true + end end require "support/display_message_matcher" From 90a3bf6b419322586d6fab13506b2f85985e6289 Mon Sep 17 00:00:00 2001 From: Andrew Gwozdziewycz Date: Fri, 19 Dec 2014 15:58:59 -0500 Subject: [PATCH 194/952] Remove syslog from drains help --- lib/heroku/command/drains.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/drains.rb b/lib/heroku/command/drains.rb index f22dd43ec..f3cd75f81 100644 --- a/lib/heroku/command/drains.rb +++ b/lib/heroku/command/drains.rb @@ -2,13 +2,13 @@ module Heroku::Command - # display syslog drains for an app + # display drains for an app # class Drains < Base # drains # - # list all syslog drains + # list all drains # def index puts heroku.list_drains(app) @@ -17,7 +17,7 @@ def index # drains:add URL # - # add a syslog drain + # add a drain # def add if url = args.shift @@ -30,7 +30,7 @@ def add # drains:remove URL # - # remove a syslog drain + # remove a drain # def remove if url = args.shift @@ -43,4 +43,3 @@ def remove end end - From 2886dab528ebffc33d5bd7e8d034c2bd0b2c4e36 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 22 Dec 2014 12:57:23 -0800 Subject: [PATCH 195/952] explicitly require multi_json --- lib/heroku/cli.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 3d61b64fc..c3b609a95 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -13,6 +13,7 @@ require "heroku/helpers" require "heroku/http_instrumentor" require 'rest_client' +require 'multi_json' require 'heroku-api' begin From dee035ff481618dbbaf355d06a1607560ae6aec8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 23 Dec 2014 17:35:41 -0800 Subject: [PATCH 196/952] warn about using out of date git --- lib/heroku/cli.rb | 18 ++++++++--------- lib/heroku/git.rb | 48 +++++++++++++++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 5 +++++ 3 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 lib/heroku/git.rb diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index c3b609a95..eab7030e0 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -8,10 +8,11 @@ exit 1 end -require "heroku" -require "heroku/command" -require "heroku/helpers" -require "heroku/http_instrumentor" +require 'heroku' +require 'heroku/command' +require 'heroku/helpers' +require 'heroku/http_instrumentor' +require 'heroku/git' require 'rest_client' require 'multi_json' require 'heroku-api' @@ -30,12 +31,9 @@ class Heroku::CLI extend Heroku::Helpers def self.start(*args) - if $stdin.isatty - $stdin.sync = true - end - if $stdout.isatty - $stdout.sync = true - end + $stdin.sync = true if $stdin.isatty + $stdout.sync = true if $stdout.isatty + Heroku::Git.check_git_version command = args.shift.strip rescue "help" Heroku::Command.load Heroku::Command.run(command, args) diff --git a/lib/heroku/git.rb b/lib/heroku/git.rb new file mode 100644 index 000000000..05d2692d5 --- /dev/null +++ b/lib/heroku/git.rb @@ -0,0 +1,48 @@ +module Heroku::Git + def self.check_git_version + v = Version.parse(git_version) + if v < Version.parse('1.9') && v < Version.parse('1.8.5.6') + warn_about_insecure_git + elsif v < Version.parse('2.0') && v < Version.parse('1.9.5') + warn_about_insecure_git + elsif v < Version.parse('2.1') && v < Version.parse('2.0.5') + warn_about_insecure_git + elsif v < Version.parse('2.2') && v < Version.parse('2.2.1') + warn_about_insecure_git + end + end + + def self.warn_about_insecure_git + warn "Your version of git is #{git_version}. Which has serious security vulnerabilities." + warn "More information here: https://blog.heroku.com/archives/2014/12/23/update_your_git_clients_on_windows_and_os_x" + end + + private + + def self.git_version + /git version ([\d\.]+)/.match(`git --version`)[1] + end + + + class Version + include Comparable + + attr_accessor :major, :minor, :patch, :special + + def initialize(major, minor=0, patch=0, special=0) + @major, @minor, @patch, @special = major, minor, patch, special + end + + def self.parse(s) + digits = s.split('.').map { |i| i.to_i } + Version.new(*digits) + end + + def <=>(other) + return major <=> other.major unless (major <=> other.major) == 0 + return minor <=> other.minor unless (minor <=> other.minor) == 0 + return patch <=> other.patch unless (patch <=> other.patch) == 0 + return special <=> other.special + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5f1a01f39..505b4f7fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -209,6 +209,11 @@ def has_http_git_entry_in_netrc end end +require "heroku/git" +module Heroku::Git + def self.check_git_version; end +end + require "support/display_message_matcher" require "support/organizations_mock_helper" From 1315f7a35f797f4d2e678e0c4e0b3e5d54177d80 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 23 Dec 2014 17:47:44 -0800 Subject: [PATCH 197/952] v3.21.2 --- CHANGELOG | 6 ++++++ lib/heroku/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 37a1d7bc9..57d92e0b5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.21.2 2014-12-23 +================= +Show warning for insecure Git clients +Fix issue with MultiJson require +Happy Festivus! + 3.21.1 2014-12-17 ================= No changes, needed to bump the version diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index aeb3a26de..1dfc3623f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.21.1" + VERSION = "3.21.2" end From 4baa7a931c8c8b7377cd79c9473b3eab82647b1b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 23 Dec 2014 17:49:04 -0800 Subject: [PATCH 198/952] v3.21.2 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a9549c6f0..6d80b68ed 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.21.1) + heroku (3.21.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) From 55d148c947c69df931263674b8569f57ca97d697 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 23 Dec 2014 17:49:54 -0800 Subject: [PATCH 199/952] v3.21.3 --- CHANGELOG | 2 +- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 57d92e0b5..487acebf2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -3.21.2 2014-12-23 +3.21.3 2014-12-23 ================= Show warning for insecure Git clients Fix issue with MultiJson require diff --git a/Gemfile.lock b/Gemfile.lock index 6d80b68ed..08bfe9b77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.21.2) + heroku (3.21.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1dfc3623f..f5ec52b18 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.21.2" + VERSION = "3.21.3" end From 2a3f836f7c487096efcb46a8934ae95103d0dc54 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 31 Dec 2014 10:42:53 -0800 Subject: [PATCH 200/952] fix bug with git warning --- lib/heroku/git.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/heroku/git.rb b/lib/heroku/git.rb index 05d2692d5..8b7da9a6b 100644 --- a/lib/heroku/git.rb +++ b/lib/heroku/git.rb @@ -1,13 +1,16 @@ module Heroku::Git def self.check_git_version v = Version.parse(git_version) - if v < Version.parse('1.9') && v < Version.parse('1.8.5.6') + if v > Version.parse('1.8') && v < Version.parse('1.8.5.6') warn_about_insecure_git - elsif v < Version.parse('2.0') && v < Version.parse('1.9.5') + end + if v > Version.parse('1.9') && v < Version.parse('1.9.5') warn_about_insecure_git - elsif v < Version.parse('2.1') && v < Version.parse('2.0.5') + end + if v > Version.parse('2.0') && v < Version.parse('2.0.5') warn_about_insecure_git - elsif v < Version.parse('2.2') && v < Version.parse('2.2.1') + end + if v > Version.parse('2.1') && v < Version.parse('2.2.1') warn_about_insecure_git end end From d493659f6687d357639e0edf85c83f8f4af01a00 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 31 Dec 2014 10:55:39 -0800 Subject: [PATCH 201/952] check os if git is incompatible Fixes #1339 --- lib/heroku/git.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/heroku/git.rb b/lib/heroku/git.rb index 8b7da9a6b..4f7b529e6 100644 --- a/lib/heroku/git.rb +++ b/lib/heroku/git.rb @@ -1,5 +1,8 @@ module Heroku::Git + extend Heroku::Helpers + def self.check_git_version + return unless running_on_windows? || running_on_a_mac? v = Version.parse(git_version) if v > Version.parse('1.8') && v < Version.parse('1.8.5.6') warn_about_insecure_git From 1a16ebc6cbb32eb75fb101b0fa3c25d3a6c39312 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 31 Dec 2014 10:59:50 -0800 Subject: [PATCH 202/952] v3.21.4 --- CHANGELOG | 6 ++++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 487acebf2..bec9a3618 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.21.4 2014-12-31 +================= +Fixed bug with git warnings +Removed git warnings for non-osx and non-windows environments +Happy New Year! + 3.21.3 2014-12-23 ================= Show warning for insecure Git clients diff --git a/Gemfile.lock b/Gemfile.lock index 08bfe9b77..a8c21e856 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.21.3) + heroku (3.21.4) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) @@ -38,7 +38,7 @@ GEM addressable (~> 2.3) mime-types (1.25.1) multi_json (1.10.1) - netrc (0.10.0) + netrc (0.10.2) rake (10.4.2) rest-client (1.6.7) mime-types (>= 1.16) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f5ec52b18..cc54afec4 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.21.3" + VERSION = "3.21.4" end From 8be47d3b5ca766e5db9bcc07ad1fa4f5509fdc17 Mon Sep 17 00:00:00 2001 From: Todd Wolfson Date: Wed, 31 Dec 2014 13:01:32 -0600 Subject: [PATCH 203/952] Added tests and fixed up logic errors in checking git insecurity --- lib/heroku/git.rb | 28 +++++++++++++++++------- spec/heroku/git_spec.rb | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 spec/heroku/git_spec.rb diff --git a/lib/heroku/git.rb b/lib/heroku/git.rb index 4f7b529e6..19a4e44e0 100644 --- a/lib/heroku/git.rb +++ b/lib/heroku/git.rb @@ -1,21 +1,33 @@ +require "heroku/helpers" + module Heroku::Git extend Heroku::Helpers def self.check_git_version return unless running_on_windows? || running_on_a_mac? - v = Version.parse(git_version) - if v > Version.parse('1.8') && v < Version.parse('1.8.5.6') + if git_is_insecure(git_version) warn_about_insecure_git end - if v > Version.parse('1.9') && v < Version.parse('1.9.5') - warn_about_insecure_git + end + + def self.git_is_insecure(version) + v = Version.parse(version) + if v < Version.parse('1.8.5.6') + return true end - if v > Version.parse('2.0') && v < Version.parse('2.0.5') - warn_about_insecure_git + if v >= Version.parse('1.9') && v < Version.parse('1.9.5') + return true end - if v > Version.parse('2.1') && v < Version.parse('2.2.1') - warn_about_insecure_git + if v >= Version.parse('2.0') && v < Version.parse('2.0.5') + return true + end + if v >= Version.parse('2.1') && v < Version.parse('2.1.4') + return true + end + if v >= Version.parse('2.2') && v < Version.parse('2.2.1') + return true end + return false end def self.warn_about_insecure_git diff --git a/spec/heroku/git_spec.rb b/spec/heroku/git_spec.rb new file mode 100644 index 000000000..eb796e71a --- /dev/null +++ b/spec/heroku/git_spec.rb @@ -0,0 +1,48 @@ +require "heroku/git" + +describe Heroku::Git do + # Secure versions from http://article.gmane.org/gmane.linux.kernel/1853266 + it "determines an insecure 1.7 version is insecure" do + expect(Heroku::Git.git_is_insecure('1.7')).to eq(true) + end + + it "determines an insecure 1.8 version is insecure" do + expect(Heroku::Git.git_is_insecure('1.8.5')).to eq(true) + end + + it "determines an secure 1.8 version is secure" do + expect(Heroku::Git.git_is_insecure('1.8.5.6')).to eq(false) + end + + it "determines an insecure 1.9 version is insecure" do + expect(Heroku::Git.git_is_insecure('1.9.3')).to eq(true) + end + + it "determines an secure 1.9 version is secure" do + expect(Heroku::Git.git_is_insecure('1.9.5')).to eq(false) + end + + it "determines an insecure 2.0 version is insecure" do + expect(Heroku::Git.git_is_insecure('2.0')).to eq(true) + end + + it "determines an secure 2.0 version is secure" do + expect(Heroku::Git.git_is_insecure('2.0.5')).to eq(false) + end + + it "determines an insecure 2.1 version is insecure" do + expect(Heroku::Git.git_is_insecure('2.1')).to eq(true) + end + + it "determines an secure 2.1 version is secure" do + expect(Heroku::Git.git_is_insecure('2.1.4')).to eq(false) + end + + it "determines an insecure 2.2 version is insecure" do + expect(Heroku::Git.git_is_insecure('2.2')).to eq(true) + end + + it "determines an secure 2.2 version is secure" do + expect(Heroku::Git.git_is_insecure('2.2.1')).to eq(false) + end +end From 88a7bdd82336101e976bc00b23247bb6189d678d Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Sun, 4 Jan 2015 16:43:42 -0800 Subject: [PATCH 204/952] Basic support for generating CSRs --- lib/heroku/command/certs.rb | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 894091e06..f91661c92 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -152,7 +152,34 @@ def rollback display "New active certificate details:" display_certificate_info(endpoint) end - + + # certs:generate DOMAIN [SUBJECT] + # + # Generate a certificate signing request and key for an app. + def generate + domain = args[0] || error("certs:generate must specify a domain") + subject = args[1] + + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + subj_args = if subject + ['-subj', subject] + else + [] + end + + system("openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) + display "Your CSR is in #{csrfile} and your key is in #{keyfile}." + display "When you've received your certificate, run:" + + if all_endpoint_domains.include? domain + display "$ heroku certs:update CERTFILE #{keyfile}" + else + display "$ heroku certs:add CERTFILE #{keyfile}" + end + end + private def current_endpoint @@ -229,5 +256,11 @@ def read_crt_and_key_bypassing_ssl_doctor def read_crt_and_key options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor end - + + def all_endpoint_domains + endpoints = heroku.ssl_endpoint_list(app) + endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } + .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } + .reduce(:+) + end end From 53056affd0eb3b392796cc1794b421b5b12f08dc Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Sun, 4 Jan 2015 17:18:26 -0800 Subject: [PATCH 205/952] Accommodate syntactically picky Rubys --- lib/heroku/command/certs.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index f91661c92..593c8618e 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -259,8 +259,8 @@ def read_crt_and_key def all_endpoint_domains endpoints = heroku.ssl_endpoint_list(app) - endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } - .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } + endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } \ + .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ .reduce(:+) end end From c69f853a3766a6cd18e43cfec2c17324d5abad99 Mon Sep 17 00:00:00 2001 From: Troels Thomsen Date: Mon, 5 Jan 2015 10:40:02 +0100 Subject: [PATCH 206/952] Disregard `HEROKU_MANAGER_URL` --- lib/heroku/client/organizations.rb | 2 +- lib/heroku/command/base.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index e0452cf81..86b9b42c2 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -230,7 +230,7 @@ def decompress_response!(response) end def manager_url - ENV['HEROKU_MANAGER_URL'] || Heroku::Auth.full_host(Heroku::Auth.host) + Heroku::Auth.full_host(Heroku::Auth.host) end end diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index ea85d9094..a3c508c60 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -255,7 +255,7 @@ def org_from_app! end def skip_org? - return false if ENV['HEROKU_CLOUD'].nil? || ENV['HEROKU_MANAGER_URL'] + return false if ENV['HEROKU_CLOUD'].nil? !%w{default production prod}.include? ENV['HEROKU_CLOUD'] end From 6bd1aa99fae7cb156a7f9684f0cf797e93f98828 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Nov 2014 21:16:14 -0800 Subject: [PATCH 207/952] jsplugins --- lib/heroku/command.rb | 2 + lib/heroku/command/plugins.rb | 41 +++++++---- lib/heroku/command/version.rb | 1 + lib/heroku/jsplugin.rb | 104 ++++++++++++++++++++++++++++ spec/heroku/command/keys_spec.rb | 3 + spec/heroku/command/plugins_spec.rb | 4 +- 6 files changed, 141 insertions(+), 14 deletions(-) create mode 100644 lib/heroku/jsplugin.rb diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 24a62b625..04b63847a 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -1,5 +1,6 @@ require 'heroku/helpers' require 'heroku/plugin' +require 'heroku/jsplugin' require 'heroku/version' require "optparse" @@ -18,6 +19,7 @@ def self.load require file end Heroku::Plugin.load! + Heroku::JSPlugin.load! unregister_commands_made_private_after_the_fact end diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 086f68113..3c3e62ccc 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -18,7 +18,8 @@ class Plugins < Base def index validate_arguments! - plugins = ::Heroku::Plugin.list + plugins = ::Heroku::JSPlugin.plugins.map { |p| "#{p[:name]}@#{p[:version]}" } + plugins.concat(::Heroku::Plugin.list) if plugins.length > 0 styled_header("Installed Plugins") @@ -38,18 +39,14 @@ def index # Installing heroku-accounts... done # def install - plugin = Heroku::Plugin.new(shift_argument) + name = shift_argument validate_arguments! - - action("Installing #{plugin.name}") do - if plugin.install - unless Heroku::Plugin.load_plugin(plugin.name) - plugin.uninstall - exit(1) - end - else - error("Could not install #{plugin.name}. Please check the URL and try again.") - end + if name =~ /\./ + # if it contains a '.' then we are assuming it is a URL + # and we should install it as a ruby plugin + ruby_plugin_install(name) + else + js_plugin_install(name) end end @@ -106,5 +103,25 @@ def update end end + private + + def js_plugin_install(name) + Heroku::JSPlugin.setup + Heroku::JSPlugin.install(name) + end + + def ruby_plugin_install(name) + action("Installing #{name}") do + plugin = Heroku::Plugin.new(name) + if plugin.install + unless Heroku::Plugin.load_plugin(plugin.name) + plugin.uninstall + exit(1) + end + else + error("Could not install #{plugin.name}. Please check the URL and try again.") + end + end + end end end diff --git a/lib/heroku/command/version.rb b/lib/heroku/command/version.rb index fe2a90626..9115376e3 100644 --- a/lib/heroku/command/version.rb +++ b/lib/heroku/command/version.rb @@ -18,6 +18,7 @@ def index validate_arguments! display(Heroku.user_agent) + display(Heroku::JSPlugin.version) if Heroku::JSPlugin.setup? plugins = Heroku::Plugin.list if plugins.length > 0 diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb new file mode 100644 index 000000000..1cd68755d --- /dev/null +++ b/lib/heroku/jsplugin.rb @@ -0,0 +1,104 @@ +class Heroku::JSPlugin + include Heroku::Helpers + + def self.setup? + File.exists? bin + end + + def self.load! + return unless setup? + this = self + commands.each do |plugin| + klass = Class.new do + def initialize(args, opts) + @args = args + @opts = opts + end + end + klass.send(:define_method, :run) do + ENV['HEROKU_APP'] = @opts[:app] + exec this.bin, "#{plugin[:topic]}:#{plugin[:command]}", *@args + end + Heroku::Command.register_namespace(:name => plugin[:topic]) + Heroku::Command.register_command( + :command => "#{plugin[:topic]}:#{plugin[:command]}", + :namespace => plugin[:topic], + :klass => klass, + :method => :run + ) + end + end + + def self.plugins + return [] unless setup? + @plugins ||= `#{bin} plugins`.lines.map do |line| + name, version = line.split + { :name => name, :version => version } + end + end + + def self.commands + @commands ||= `#{bin} commands`.split.flat_map do |l| + l.scan(/(\w+):(\w+)/).collect do |topic, command| + { :topic => topic, :command => command } + end + end + end + + def self.install(name) + system "#{bin} plugins:install #{name}" + end + + def self.version + `#{bin} version` + end + + def self.bin + File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") + end + + def self.setup + return if File.exist? bin + FileUtils.mkdir_p File.dirname(bin) + resp = Excon.get(url, :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + open(bin, "wb") do |file| + file.write(resp.body) + end + File.chmod(0755, bin) + if Digest::SHA1.file(bin).hexdigest != manifest['builds'][os][arch]['sha1'] + File.delete bin + raise 'SHA mismatch for heroku-cli' + end + end + + def self.arch + case RUBY_PLATFORM + when /i386/ + "386" + when /x64/ + else + "amd64" + end + end + + def self.os + case RUBY_PLATFORM + when /darwin|mac os/ + "darwin" + when /linux/ + "linux" + when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ + "windows" + else + raise "unsupported on #{RUBY_PLATFORM}" + end + end + + def self.manifest + @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/heroku-cli/master/manifest.json").body) + end + + def self.url + manifest['builds'][os][arch]['url'] + ".gz" + end +end diff --git a/spec/heroku/command/keys_spec.rb b/spec/heroku/command/keys_spec.rb index bb4d5d210..a5221544a 100644 --- a/spec/heroku/command/keys_spec.rb +++ b/spec/heroku/command/keys_spec.rb @@ -24,6 +24,9 @@ module Heroku::Command end it "adds a key from a specified keyfile path" do + # This is because the JSPlugin makes a call to File.exists + # Not pretty, but will always work and should be temporary + allow(Heroku::JSPlugin).to receive(:setup?).and_return(false) expect(File).to receive(:exists?).with('.git').and_return(false) expect(File).to receive(:exists?).with('/my/key.pub').and_return(true) expect(File).to receive(:read).with('/my/key.pub').and_return(KEY) diff --git a/spec/heroku/command/plugins_spec.rb b/spec/heroku/command/plugins_spec.rb index 48dfd65ae..40263dcf1 100644 --- a/spec/heroku/command/plugins_spec.rb +++ b/spec/heroku/command/plugins_spec.rb @@ -22,7 +22,7 @@ module Heroku::Command stderr, stdout = execute("plugins:install git://github.com/heroku/Plugin.git") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Installing Plugin... done +Installing git://github.com/heroku/Plugin.git... done STDOUT end @@ -31,7 +31,7 @@ module Heroku::Command expect(@plugin).to receive(:uninstall).and_return(true) stderr, stdout = execute("plugins:install git://github.com/heroku/Plugin.git") expect(stderr).to eq('') # normally would have error, but mocks/stubs don't allow - expect(stdout).to eq("Installing Plugin... ") # also inaccurate, would end in ' failed' + expect(stdout).to eq("Installing git://github.com/heroku/Plugin.git... ") # also inaccurate, would end in ' failed' end end From 8252a3eda65331168a8de0100ff2df259410f81f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 5 Jan 2015 15:15:04 -0800 Subject: [PATCH 208/952] v3.22.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bec9a3618..a4fb70f93 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.22.0 2015-01-05 +================= +Added JavaScript-based plugin support + 3.21.4 2014-12-31 ================= Fixed bug with git warnings diff --git a/Gemfile.lock b/Gemfile.lock index a8c21e856..fd10d5b70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.21.4) + heroku (3.22.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index cc54afec4..3546aa2b1 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.21.4" + VERSION = "3.22.0" end From 09d5d83cccb423b5cbc33a366be3708d4f1e8e54 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 5 Jan 2015 15:38:08 -0800 Subject: [PATCH 209/952] updated cacert.pem --- data/cacert.pem | 502 +++++++++++++++++++++++++----------------------- 1 file changed, 265 insertions(+), 237 deletions(-) diff --git a/data/cacert.pem b/data/cacert.pem index b775088c0..6b981e8aa 100644 --- a/data/cacert.pem +++ b/data/cacert.pem @@ -1,19 +1,22 @@ ## DOWNLOADED FROM: http://curl.haxx.se/ca/cacert.pem ## -## ca-bundle.crt -- Bundle of CA Root Certificates +## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Apr 22 08:29:31 2014 +## Certificate data from Mozilla downloaded on: Wed Sep 3 03:12:03 2014 ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates ## file (certdata.txt). This file can be found in the mozilla source tree: -## http://mxr.mozilla.org/mozilla-release/source/security/nss/lib/ckfw/builtins/certdata.txt?raw=1 +## http://hg.mozilla.org/releases/mozilla-release/raw-file/default/security/nss/lib/ckfw/builtins/certdata.txt ## ## It contains the certificates in PEM format and therefore ## can be directly used with curl / libcurl / php_curl, or with ## an Apache+mod_ssl webserver for SSL client authentication. ## Just configure this file as the SSLCACertificateFile. ## +## Conversion done with mk-ca-bundle.pl verison 1.22. +## SHA1: c4540021427a6fa29e5f50db9f12d48c97d33889 +## GTE CyberTrust Global Root @@ -91,22 +94,6 @@ BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95 70+sB3c4 -----END CERTIFICATE----- -Verisign Class 3 Public Primary Certification Authority -======================================================= ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEHC65B0Q2Sk0tjjKewPMur8wDQYJKoZIhvcNAQECBQAwXzELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMTIzNTk1OVow -XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 -f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol -hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBAgUAA4GBALtMEivPLCYA -TxQT3ab7/AoRhIzzKBxnki98tsX63/Dolbwdj2wsqFHMc9ikwFPwTtYmwHYBV4GSXiHx0bH/59Ah -WM1pF+NEHJwZRDmJXNycAA9WjQKZ7aKQRUzkuxCkPfAyAw7xzvjoyVGM5mKf5p/AfbdynMk2Omuf -Tqj/ZA1k ------END CERTIFICATE----- - Verisign Class 3 Public Primary Certification Authority - G2 ============================================================ -----BEGIN CERTIFICATE----- @@ -169,63 +156,6 @@ BgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- -ValiCert Class 1 VA -=================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDEgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNTIy -MjM0OFoXDTE5MDYyNTIyMjM0OFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDEg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDYWYJ6ibiWuqYvaG9YLqdUHAZu9OqNSLwxlBfw8068srg1knaw0KWlAdcAAxIi -GQj4/xEjm84H9b9pGib+TunRf50sQB1ZaG6m+FiwnRqP0z/x3BkGgagO4DrdyFNFCQbmD3DD+kCm -DuJWBQ8YTfwggtFzVXSNdnKgHZ0dwN0/cQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFBoPUn0LBwG -lN+VYH+Wexf+T3GtZMjdd9LvWVXoP+iOBSoh8gfStadS/pyxtuJbdxdA6nLWI8sogTLDAHkY7FkX -icnGah5xyf23dKUlRWnFSKsZ4UWKJWsZ7uW7EvV/96aNUcPwnXS3qT6gpf+2SQMT2iLM7XGCK5nP -Orf1LXLI ------END CERTIFICATE----- - -ValiCert Class 2 VA -=================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDIgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw -MTk1NFoXDTE5MDYyNjAwMTk1NFowgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDIg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDOOnHK5avIWZJV16vYdA757tn2VUdZZUcOBVXc65g2PFxTXdMwzzjsvUGJ7SVC -CSRrCl6zfN1SLUzm1NZ9WlmpZdRJEy0kTRxQb7XBhVQ7/nHk01xC+YDgkRoKWzk2Z/M/VXwbP7Rf -ZHM047QSv4dk+NoS/zcnwbNDu+97bi5p9wIDAQABMA0GCSqGSIb3DQEBBQUAA4GBADt/UG9vUJSZ -SWI4OB9L+KXIPqeCgfYrx+jFzug6EILLGACOTb2oWH+heQC1u+mNr0HZDzTuIYEZoDJJKPTEjlbV -UjP9UNV+mWwD5MlM/Mtsq2azSiGM5bUMMj4QssxsodyamEwCW/POuZ6lcg5Ktz885hZo+L7tdEy8 -W9ViH0Pd ------END CERTIFICATE----- - -RSA Root Certificate 1 -====================== ------BEGIN CERTIFICATE----- -MIIC5zCCAlACAQEwDQYJKoZIhvcNAQEFBQAwgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRp -b24gTmV0d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENs -YXNzIDMgUG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZh -bGljZXJ0LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMB4XDTk5MDYyNjAw -MjIzM1oXDTE5MDYyNjAwMjIzM1owgbsxJDAiBgNVBAcTG1ZhbGlDZXJ0IFZhbGlkYXRpb24gTmV0 -d29yazEXMBUGA1UEChMOVmFsaUNlcnQsIEluYy4xNTAzBgNVBAsTLFZhbGlDZXJ0IENsYXNzIDMg -UG9saWN5IFZhbGlkYXRpb24gQXV0aG9yaXR5MSEwHwYDVQQDExhodHRwOi8vd3d3LnZhbGljZXJ0 -LmNvbS8xIDAeBgkqhkiG9w0BCQEWEWluZm9AdmFsaWNlcnQuY29tMIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDjmFGWHOjVsQaBalfDcnWTq8+epvzzFlLWLU2fNUSoLgRNB0mKOCn1dzfnt6td -3zZxFJmP3MKS8edgkpfs2Ejcv8ECIMYkpChMMFp2bbFc893enhBxoYjHW5tBbcqwuI4V7q0zK89H -BFx1cQqYJJgpp0lZpd34t0NiYfPT4tBVPwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAFa7AliEZwgs -3x/be0kz9dNnnfS0ChCzycUs4pJqcXgn8nCDQtM+z6lU9PHYkhaM0QTLS6vJn0WuPIqpsHEzXcjF -V9+vqDWzf4mH6eglkrh/hXqu1rweN1gqZ8mRzyqBPu3GOd/APhmcGcwTTYJBtYze4D1gCCAPRX5r -on+jjBXu ------END CERTIFICATE----- - Verisign Class 3 Public Primary Certification Authority - G3 ============================================================ -----BEGIN CERTIFICATE----- @@ -274,33 +204,6 @@ RTjDOPP8hS6DRkiy1yBfkjaP53kPmF6Z6PDQpLv1U70qzlmwr25/bLvSHgCwIe34QWKCudiyxLtG UPMxxY8BqHTr9Xgn2uf3ZkPznoM+IKrDNWCRzg== -----END CERTIFICATE----- -Entrust.net Secure Server CA -============================ ------BEGIN CERTIFICATE----- -MIIE2DCCBEGgAwIBAgIEN0rSQzANBgkqhkiG9w0BAQUFADCBwzELMAkGA1UEBhMCVVMxFDASBgNV -BAoTC0VudHJ1c3QubmV0MTswOQYDVQQLEzJ3d3cuZW50cnVzdC5uZXQvQ1BTIGluY29ycC4gYnkg -cmVmLiAobGltaXRzIGxpYWIuKTElMCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRl -ZDE6MDgGA1UEAxMxRW50cnVzdC5uZXQgU2VjdXJlIFNlcnZlciBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eTAeFw05OTA1MjUxNjA5NDBaFw0xOTA1MjUxNjM5NDBaMIHDMQswCQYDVQQGEwJVUzEUMBIG -A1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5ldC9DUFMgaW5jb3JwLiBi -eSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBFbnRydXN0Lm5ldCBMaW1p -dGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDNKIM0VBuJ8w+vN5Ex/68xYMmo6LIQ -aO2f55M28Qpku0f1BBc/I0dNxScZgSYMVHINiC3ZH5oSn7yzcdOAGT9HZnuMNSjSuQrfJNqc1lB5 -gXpa0zf3wkrYKZImZNHkmGw6AIr1NJtl+O3jEP/9uElY3KDegjlrgbEWGWG5VLbmQwIBA6OCAdcw -ggHTMBEGCWCGSAGG+EIBAQQEAwIABzCCARkGA1UdHwSCARAwggEMMIHeoIHboIHYpIHVMIHSMQsw -CQYDVQQGEwJVUzEUMBIGA1UEChMLRW50cnVzdC5uZXQxOzA5BgNVBAsTMnd3dy5lbnRydXN0Lm5l -dC9DUFMgaW5jb3JwLiBieSByZWYuIChsaW1pdHMgbGlhYi4pMSUwIwYDVQQLExwoYykgMTk5OSBF -bnRydXN0Lm5ldCBMaW1pdGVkMTowOAYDVQQDEzFFbnRydXN0Lm5ldCBTZWN1cmUgU2VydmVyIENl -cnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMCmgJ6AlhiNodHRwOi8vd3d3LmVu -dHJ1c3QubmV0L0NSTC9uZXQxLmNybDArBgNVHRAEJDAigA8xOTk5MDUyNTE2MDk0MFqBDzIwMTkw -NTI1MTYwOTQwWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAU8BdiE1U9s/8KAGv7UISX8+1i0Bow -HQYDVR0OBBYEFPAXYhNVPbP/CgBr+1CEl/PtYtAaMAwGA1UdEwQFMAMBAf8wGQYJKoZIhvZ9B0EA -BAwwChsEVjQuMAMCBJAwDQYJKoZIhvcNAQEFBQADgYEAkNwwAvpkdMKnCqV8IY00F6j7Rw7/JXyN -Ewr75Ji174z4xRAN95K+8cPV1ZVqBLssziY2ZcgxxufuP+NXdYR6Ee9GTxj005i7qIcyunL2POI9 -n9cd2cNgQ4xYDiKWL2KjLB+6rQXvqzJ4h6BUcxm1XAX5Uj5tLUUL9wqT6u0G+bI= ------END CERTIFICATE----- - Entrust.net Premium 2048 Secure Server CA ========================================= -----BEGIN CERTIFICATE----- @@ -954,30 +857,6 @@ nGQI0DvDKcWy7ZAEwbEpkcUwb8GpcjPM/l0WFywRaed+/sWDCN+83CI6LiBpIzlWYGeQiy52OfsR iJf2fL1LuCAWZwWN4jvBcj+UlTfHXbme2JOhF4//DGYVwSR8MnwDHTuhWEUykw== -----END CERTIFICATE----- -TDC Internet Root CA -==================== ------BEGIN CERTIFICATE----- -MIIEKzCCAxOgAwIBAgIEOsylTDANBgkqhkiG9w0BAQUFADBDMQswCQYDVQQGEwJESzEVMBMGA1UE -ChMMVERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTAeFw0wMTA0MDUx -NjMzMTdaFw0yMTA0MDUxNzAzMTdaMEMxCzAJBgNVBAYTAkRLMRUwEwYDVQQKEwxUREMgSW50ZXJu -ZXQxHTAbBgNVBAsTFFREQyBJbnRlcm5ldCBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAxLhAvJHVYx/XmaCLDEAedLdInUaMArLgJF/wGROnN4NrXceO+YQwzho7+vvOi20j -xsNuZp+Jpd/gQlBn+h9sHvTQBda/ytZO5GhgbEaqHF1j4QeGDmUApy6mcca8uYGoOn0a0vnRrEvL -znWv3Hv6gXPU/Lq9QYjUdLP5Xjg6PEOo0pVOd20TDJ2PeAG3WiAfAzc14izbSysseLlJ28TQx5yc -5IogCSEWVmb/Bexb4/DPqyQkXsN/cHoSxNK1EKC2IeGNeGlVRGn1ypYcNIUXJXfi9i8nmHj9eQY6 -otZaQ8H/7AQ77hPv01ha/5Lr7K7a8jcDR0G2l8ktCkEiu7vmpwIDAQABo4IBJTCCASEwEQYJYIZI -AYb4QgEBBAQDAgAHMGUGA1UdHwReMFwwWqBYoFakVDBSMQswCQYDVQQGEwJESzEVMBMGA1UEChMM -VERDIEludGVybmV0MR0wGwYDVQQLExRUREMgSW50ZXJuZXQgUm9vdCBDQTENMAsGA1UEAxMEQ1JM -MTArBgNVHRAEJDAigA8yMDAxMDQwNTE2MzMxN1qBDzIwMjEwNDA1MTcwMzE3WjALBgNVHQ8EBAMC -AQYwHwYDVR0jBBgwFoAUbGQBx/2FbazI2p5QCIUItTxWqFAwHQYDVR0OBBYEFGxkAcf9hW2syNqe -UAiFCLU8VqhQMAwGA1UdEwQFMAMBAf8wHQYJKoZIhvZ9B0EABBAwDhsIVjUuMDo0LjADAgSQMA0G -CSqGSIb3DQEBBQUAA4IBAQBOQ8zR3R0QGwZ/t6T609lN+yOfI1Rb5osvBCiLtSdtiaHsmGnc540m -gwV5dOy0uaOXwTUA/RXaOYE6lTGQ3pfphqiZdwzlWqCE/xIWrG64jcN7ksKsLtB9KOy282A4aW8+ -2ARVPp7MVdK6/rtHBNcK2RYKNCn1WBPVT8+PVkuzHu7TmHnaCB4Mb7j4Fifvwm899qNLPg7kbWzb -O0ESm70NRyN/PErQr8Cv9u8btRXE64PECV90i9kR+8JWsTz4cMo0jUNAE4z9mQNUecYu6oah9jrU -Cbz0vGbMPVjQV0kK7iXiQe4T+Zs4NNEA9X7nlB38aQNiuJkFBT1reBK9sG9l ------END CERTIFICATE----- - UTN DATACorp SGC Root CA ======================== -----BEGIN CERTIFICATE----- @@ -1118,64 +997,6 @@ KuZoPL9coAob4Q566eKAw+np9v1sEZ7Q5SgnK1QyQhSCdeZK8CtmdWOMovsEPoMOmzbwGOQmIMOM 8CgHrTwXZoi1/baI -----END CERTIFICATE----- -NetLock Business (Class B) Root -=============================== ------BEGIN CERTIFICATE----- -MIIFSzCCBLSgAwIBAgIBaTANBgkqhkiG9w0BAQQFADCBmTELMAkGA1UEBhMCSFUxETAPBgNVBAcT -CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV -BAsTEVRhbnVzaXR2YW55a2lhZG9rMTIwMAYDVQQDEylOZXRMb2NrIFV6bGV0aSAoQ2xhc3MgQikg -VGFudXNpdHZhbnlraWFkbzAeFw05OTAyMjUxNDEwMjJaFw0xOTAyMjAxNDEwMjJaMIGZMQswCQYD -VQQGEwJIVTERMA8GA1UEBxMIQnVkYXBlc3QxJzAlBgNVBAoTHk5ldExvY2sgSGFsb3phdGJpenRv -bnNhZ2kgS2Z0LjEaMBgGA1UECxMRVGFudXNpdHZhbnlraWFkb2sxMjAwBgNVBAMTKU5ldExvY2sg -VXpsZXRpIChDbGFzcyBCKSBUYW51c2l0dmFueWtpYWRvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB -iQKBgQCx6gTsIKAjwo84YM/HRrPVG/77uZmeBNwcf4xKgZjupNTKihe5In+DCnVMm8Bp2GQ5o+2S -o/1bXHQawEfKOml2mrriRBf8TKPV/riXiK+IA4kfpPIEPsgHC+b5sy96YhQJRhTKZPWLgLViqNhr -1nGTLbO/CVRY7QbrqHvcQ7GhaQIDAQABo4ICnzCCApswEgYDVR0TAQH/BAgwBgEB/wIBBDAOBgNV -HQ8BAf8EBAMCAAYwEQYJYIZIAYb4QgEBBAQDAgAHMIICYAYJYIZIAYb4QgENBIICURaCAk1GSUdZ -RUxFTSEgRXplbiB0YW51c2l0dmFueSBhIE5ldExvY2sgS2Z0LiBBbHRhbGFub3MgU3pvbGdhbHRh -dGFzaSBGZWx0ZXRlbGVpYmVuIGxlaXJ0IGVsamFyYXNvayBhbGFwamFuIGtlc3p1bHQuIEEgaGl0 -ZWxlc2l0ZXMgZm9seWFtYXRhdCBhIE5ldExvY2sgS2Z0LiB0ZXJtZWtmZWxlbG9zc2VnLWJpenRv -c2l0YXNhIHZlZGkuIEEgZGlnaXRhbGlzIGFsYWlyYXMgZWxmb2dhZGFzYW5hayBmZWx0ZXRlbGUg -YXogZWxvaXJ0IGVsbGVub3J6ZXNpIGVsamFyYXMgbWVndGV0ZWxlLiBBeiBlbGphcmFzIGxlaXJh -c2EgbWVndGFsYWxoYXRvIGEgTmV0TG9jayBLZnQuIEludGVybmV0IGhvbmxhcGphbiBhIGh0dHBz -Oi8vd3d3Lm5ldGxvY2submV0L2RvY3MgY2ltZW4gdmFneSBrZXJoZXRvIGF6IGVsbGVub3J6ZXNA -bmV0bG9jay5uZXQgZS1tYWlsIGNpbWVuLiBJTVBPUlRBTlQhIFRoZSBpc3N1YW5jZSBhbmQgdGhl -IHVzZSBvZiB0aGlzIGNlcnRpZmljYXRlIGlzIHN1YmplY3QgdG8gdGhlIE5ldExvY2sgQ1BTIGF2 -YWlsYWJsZSBhdCBodHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIG9yIGJ5IGUtbWFpbCBhdCBj -cHNAbmV0bG9jay5uZXQuMA0GCSqGSIb3DQEBBAUAA4GBAATbrowXr/gOkDFOzT4JwG06sPgzTEdM -43WIEJessDgVkcYplswhwG08pXTP2IKlOcNl40JwuyKQ433bNXbhoLXan3BukxowOR0w2y7jfLKR -stE3Kfq51hdcR0/jHTjrn9V7lagonhVK0dHQKwCXoOKSNitjrFgBazMpUIaD8QFI ------END CERTIFICATE----- - -NetLock Express (Class C) Root -============================== ------BEGIN CERTIFICATE----- -MIIFTzCCBLigAwIBAgIBaDANBgkqhkiG9w0BAQQFADCBmzELMAkGA1UEBhMCSFUxETAPBgNVBAcT -CEJ1ZGFwZXN0MScwJQYDVQQKEx5OZXRMb2NrIEhhbG96YXRiaXp0b25zYWdpIEtmdC4xGjAYBgNV -BAsTEVRhbnVzaXR2YW55a2lhZG9rMTQwMgYDVQQDEytOZXRMb2NrIEV4cHJlc3N6IChDbGFzcyBD -KSBUYW51c2l0dmFueWtpYWRvMB4XDTk5MDIyNTE0MDgxMVoXDTE5MDIyMDE0MDgxMVowgZsxCzAJ -BgNVBAYTAkhVMREwDwYDVQQHEwhCdWRhcGVzdDEnMCUGA1UEChMeTmV0TG9jayBIYWxvemF0Yml6 -dG9uc2FnaSBLZnQuMRowGAYDVQQLExFUYW51c2l0dmFueWtpYWRvazE0MDIGA1UEAxMrTmV0TG9j -ayBFeHByZXNzeiAoQ2xhc3MgQykgVGFudXNpdHZhbnlraWFkbzCBnzANBgkqhkiG9w0BAQEFAAOB -jQAwgYkCgYEA6+ywbGGKIyWvYCDj2Z/8kwvbXY2wobNAOoLO/XXgeDIDhlqGlZHtU/qdQPzm6N3Z -W3oDvV3zOwzDUXmbrVWg6dADEK8KuhRC2VImESLH0iDMgqSaqf64gXadarfSNnU+sYYJ9m5tfk63 -euyucYT2BDMIJTLrdKwWRMbkQJMdf60CAwEAAaOCAp8wggKbMBIGA1UdEwEB/wQIMAYBAf8CAQQw -DgYDVR0PAQH/BAQDAgAGMBEGCWCGSAGG+EIBAQQEAwIABzCCAmAGCWCGSAGG+EIBDQSCAlEWggJN -RklHWUVMRU0hIEV6ZW4gdGFudXNpdHZhbnkgYSBOZXRMb2NrIEtmdC4gQWx0YWxhbm9zIFN6b2xn -YWx0YXRhc2kgRmVsdGV0ZWxlaWJlbiBsZWlydCBlbGphcmFzb2sgYWxhcGphbiBrZXN6dWx0LiBB -IGhpdGVsZXNpdGVzIGZvbHlhbWF0YXQgYSBOZXRMb2NrIEtmdC4gdGVybWVrZmVsZWxvc3NlZy1i -aXp0b3NpdGFzYSB2ZWRpLiBBIGRpZ2l0YWxpcyBhbGFpcmFzIGVsZm9nYWRhc2FuYWsgZmVsdGV0 -ZWxlIGF6IGVsb2lydCBlbGxlbm9yemVzaSBlbGphcmFzIG1lZ3RldGVsZS4gQXogZWxqYXJhcyBs -ZWlyYXNhIG1lZ3RhbGFsaGF0byBhIE5ldExvY2sgS2Z0LiBJbnRlcm5ldCBob25sYXBqYW4gYSBo -dHRwczovL3d3dy5uZXRsb2NrLm5ldC9kb2NzIGNpbWVuIHZhZ3kga2VyaGV0byBheiBlbGxlbm9y -emVzQG5ldGxvY2submV0IGUtbWFpbCBjaW1lbi4gSU1QT1JUQU5UISBUaGUgaXNzdWFuY2UgYW5k -IHRoZSB1c2Ugb2YgdGhpcyBjZXJ0aWZpY2F0ZSBpcyBzdWJqZWN0IHRvIHRoZSBOZXRMb2NrIENQ -UyBhdmFpbGFibGUgYXQgaHR0cHM6Ly93d3cubmV0bG9jay5uZXQvZG9jcyBvciBieSBlLW1haWwg -YXQgY3BzQG5ldGxvY2submV0LjANBgkqhkiG9w0BAQQFAAOBgQAQrX/XDDKACtiG8XmYta3UzbM2 -xJZIwVzNmtkFLp++UOv0JhQQLdRmF/iewSf98e3ke0ugbLWrmldwpu2gpO0u9f38vf5NNwgMvOOW -gyL1SRt/Syu0VMGAfJlOHdCM7tCs5ZL6dVb+ZKATj7i4Fp1hBWeAyNDYpQcCNJgEjTME1A== ------END CERTIFICATE----- - XRamp Global CA Root ==================== -----BEGIN CERTIFICATE----- @@ -1930,40 +1751,6 @@ PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- -AC Ra\xC3\xADz Certic\xC3\xA1mara S.A. -====================================== ------BEGIN CERTIFICATE----- -MIIGZjCCBE6gAwIBAgIPB35Sk3vgFeNX8GmMy+wMMA0GCSqGSIb3DQEBBQUAMHsxCzAJBgNVBAYT -AkNPMUcwRQYDVQQKDD5Tb2NpZWRhZCBDYW1lcmFsIGRlIENlcnRpZmljYWNpw7NuIERpZ2l0YWwg -LSBDZXJ0aWPDoW1hcmEgUy5BLjEjMCEGA1UEAwwaQUMgUmHDrXogQ2VydGljw6FtYXJhIFMuQS4w -HhcNMDYxMTI3MjA0NjI5WhcNMzAwNDAyMjE0MjAyWjB7MQswCQYDVQQGEwJDTzFHMEUGA1UECgw+ -U29jaWVkYWQgQ2FtZXJhbCBkZSBDZXJ0aWZpY2FjacOzbiBEaWdpdGFsIC0gQ2VydGljw6FtYXJh -IFMuQS4xIzAhBgNVBAMMGkFDIFJhw616IENlcnRpY8OhbWFyYSBTLkEuMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEAq2uJo1PMSCMI+8PPUZYILrgIem08kBeGqentLhM0R7LQcNzJPNCN -yu5LF6vQhbCnIwTLqKL85XXbQMpiiY9QngE9JlsYhBzLfDe3fezTf3MZsGqy2IiKLUV0qPezuMDU -2s0iiXRNWhU5cxh0T7XrmafBHoi0wpOQY5fzp6cSsgkiBzPZkc0OnB8OIMfuuzONj8LSWKdf/WU3 -4ojC2I+GdV75LaeHM/J4Ny+LvB2GNzmxlPLYvEqcgxhaBvzz1NS6jBUJJfD5to0EfhcSM2tXSExP -2yYe68yQ54v5aHxwD6Mq0Do43zeX4lvegGHTgNiRg0JaTASJaBE8rF9ogEHMYELODVoqDA+bMMCm -8Ibbq0nXl21Ii/kDwFJnmxL3wvIumGVC2daa49AZMQyth9VXAnow6IYm+48jilSH5L887uvDdUhf -HjlvgWJsxS3EF1QZtzeNnDeRyPYL1epjb4OsOMLzP96a++EjYfDIJss2yKHzMI+ko6Kh3VOz3vCa -Mh+DkXkwwakfU5tTohVTP92dsxA7SH2JD/ztA/X7JWR1DhcZDY8AFmd5ekD8LVkH2ZD6mq093ICK -5lw1omdMEWux+IBkAC1vImHFrEsm5VoQgpukg3s0956JkSCXjrdCx2bD0Omk1vUgjcTDlaxECp1b -czwmPS9KvqfJpxAe+59QafMCAwEAAaOB5jCB4zAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE -AwIBBjAdBgNVHQ4EFgQU0QnQ6dfOeXRU+Tows/RtLAMDG2gwgaAGA1UdIASBmDCBlTCBkgYEVR0g -ADCBiTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5jZXJ0aWNhbWFyYS5jb20vZHBjLzBaBggrBgEF -BQcCAjBOGkxMaW1pdGFjaW9uZXMgZGUgZ2FyYW507WFzIGRlIGVzdGUgY2VydGlmaWNhZG8gc2Ug -cHVlZGVuIGVuY29udHJhciBlbiBsYSBEUEMuMA0GCSqGSIb3DQEBBQUAA4ICAQBclLW4RZFNjmEf -AygPU3zmpFmps4p6xbD/CHwso3EcIRNnoZUSQDWDg4902zNc8El2CoFS3UnUmjIz75uny3XlesuX -EpBcunvFm9+7OSPI/5jOCk0iAUgHforA1SBClETvv3eiiWdIG0ADBaGJ7M9i4z0ldma/Jre7Ir5v -/zlXdLp6yQGVwZVR6Kss+LGGIOk/yzVb0hfpKv6DExdA7ohiZVvVO2Dpezy4ydV/NgIlqmjCMRW3 -MGXrfx1IebHPOeJCgBbT9ZMj/EyXyVo3bHwi2ErN0o42gzmRkBDI8ck1fj+404HGIGQatlDCIaR4 -3NAvO2STdPCWkPHv+wlaNECW8DYSwaN0jJN+Qd53i+yG2dIPPy3RzECiiWZIHiCznCNZc6lEc7wk -eZBWN7PGKX6jD/EpOe9+XCgycDWs2rjIdWb8m0w5R44bb5tNAlQiM+9hup4phO9OSzNHdpdqy35f -/RWmnkJDW2ZaiogN9xa5P1FlK2Zqi9E4UqLWRhH6/JocdJ6PlwsCT2TG9WjTSy3/pDceiz+/RL5h -RqGEPQgnTIEgd4kI6mdAXmwIUV80WoyWaM3X94nCHNMyAK9Sy9NgWyo6R35rMDOhYil/SrnhLecU -Iw4OGEfhefwVVdCx/CVxY3UzHCMrr1zZ7Ud3YA47Dx7SwNxkBYn8eNZcLCZDqQ== ------END CERTIFICATE----- - TC TrustCenter Class 2 CA II ============================ -----BEGIN CERTIFICATE----- @@ -2431,7 +2218,7 @@ A2gAMGUCMGYhDBgmYFo4e1ZC4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIx AJw9SDkjOVgaFRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== -----END CERTIFICATE----- -NetLock Arany (Class Gold) Főtanúsítvány +NetLock Arany (Class Gold) FÅ‘tanúsítvány ============================================ -----BEGIN CERTIFICATE----- MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G @@ -2611,22 +2398,6 @@ MCwXEGCSn1WHElkQwg9naRHMTh5+Spqtr0CodaxWkHS4oJyleW/c6RrIaQXpuvoDs3zk4E7Czp3o tkYNbn5XOmeUwssfnHdKZ05phkOTOPu220+DkdRgfks+KzgHVZhepA== -----END CERTIFICATE----- -Verisign Class 3 Public Primary Certification Authority -======================================================= ------BEGIN CERTIFICATE----- -MIICPDCCAaUCEDyRMcsf9tAbDpq40ES/Er4wDQYJKoZIhvcNAQEFBQAwXzELMAkGA1UEBhMCVVMx -FzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAzIFB1YmxpYyBQcmltYXJ5 -IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTk2MDEyOTAwMDAwMFoXDTI4MDgwMjIzNTk1OVow -XzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMTcwNQYDVQQLEy5DbGFzcyAz -IFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIGfMA0GCSqGSIb3DQEBAQUA -A4GNADCBiQKBgQDJXFme8huKARS0EN8EQNvjV69qRUCPhAwL0TPZ2RHP7gJYHyX3KqhEBarsAx94 -f56TuZoAqiN91qyFomNFx3InzPRMxnVx0jnvT0Lwdd8KkMaOIG+YD/isI19wKTakyYbnsZogy1Ol -hec9vn2a/iRFM9x2Fe0PonFkTGUugWhFpwIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABByUqkFFBky -CEHwxWsKzH4PIRnN5GfcX6kb5sroc50i2JhucwNhkcV8sEVAbkSdjbCxlnRhLQ2pRdKkkirWmnWX -bj9T/UWZYB2oK0z5XqcJ2HUw19JlYD1n1khVdWk/kfVIC0dpImmClr7JyDiGSnoscxlIaU5rfGW/ -D/xwzoiQ ------END CERTIFICATE----- - Microsec e-Szigno Root CA 2009 ============================== -----BEGIN CERTIFICATE----- @@ -3009,7 +2780,7 @@ Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI 03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= -----END CERTIFICATE----- -Certinomis - Autorité Racine +Certinomis - Autorité Racine ============================= -----BEGIN CERTIFICATE----- MIIFnDCCA4SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJGUjETMBEGA1UEChMK @@ -3865,3 +3636,260 @@ TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G 3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed -----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +WoSign +====== +-----BEGIN CERTIFICATE----- +MIIFdjCCA16gAwIBAgIQXmjWEXGUY1BWAGjzPsnFkTANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQG +EwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxKjAoBgNVBAMTIUNlcnRpZmljYXRpb24g +QXV0aG9yaXR5IG9mIFdvU2lnbjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgwMTAwMDFaMFUxCzAJ +BgNVBAYTAkNOMRowGAYDVQQKExFXb1NpZ24gQ0EgTGltaXRlZDEqMCgGA1UEAxMhQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkgb2YgV29TaWduMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +vcqNrLiRFVaXe2tcesLea9mhsMMQI/qnobLMMfo+2aYpbxY94Gv4uEBf2zmoAHqLoE1UfcIiePyO +CbiohdfMlZdLdNiefvAA5A6JrkkoRBoQmTIPJYhTpA2zDxIIFgsDcSccf+Hb0v1naMQFXQoOXXDX +2JegvFNBmpGN9J42Znp+VsGQX+axaCA2pIwkLCxHC1l2ZjC1vt7tj/id07sBMOby8w7gLJKA84X5 +KIq0VC6a7fd2/BVoFutKbOsuEo/Uz/4Mx1wdC34FMr5esAkqQtXJTpCzWQ27en7N1QhatH/YHGkR ++ScPewavVIMYe+HdVHpRaG53/Ma/UkpmRqGyZxq7o093oL5d//xWC0Nyd5DKnvnyOfUNqfTq1+ez +EC8wQjchzDBwyYaYD8xYTYO7feUapTeNtqwylwA6Y3EkHp43xP901DfA4v6IRmAR3Qg/UDaruHqk +lWJqbrDKaiFaafPz+x1wOZXzp26mgYmhiMU7ccqjUu6Du/2gd/Tkb+dC221KmYo0SLwX3OSACCK2 +8jHAPwQ+658geda4BmRkAjHXqc1S+4RFaQkAKtxVi8QGRkvASh0JWzko/amrzgD5LkhLJuYwTKVY +yrREgk/nkR4zw7CT/xH8gdLKH3Ep3XZPkiWvHYG3Dy+MwwbMLyejSuQOmbp8HkUff6oZRZb9/D0C +AwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOFmzw7R +8bNLtwYgFP6HEtX2/vs+MA0GCSqGSIb3DQEBBQUAA4ICAQCoy3JAsnbBfnv8rWTjMnvMPLZdRtP1 +LOJwXcgu2AZ9mNELIaCJWSQBnfmvCX0KI4I01fx8cpm5o9dU9OpScA7F9dY74ToJMuYhOZO9sxXq +T2r09Ys/L3yNWC7F4TmgPsc9SnOeQHrAK2GpZ8nzJLmzbVUsWh2eJXLOC62qx1ViC777Y7NhRCOj +y+EaDveaBk3e1CNOIZZbOVtXHS9dCF4Jef98l7VNg64N1uajeeAz0JmWAjCnPv/So0M/BVoG6kQC +2nz4SNAzqfkHx5Xh9T71XXG68pWpdIhhWeO/yloTunK0jF02h+mmxTwTv97QRCbut+wucPrXnbes +5cVAWubXbHssw1abR80LzvobtCHXt2a49CUwi1wNuepnsvRtrtWhnk/Yn+knArAdBtaP4/tIEp9/ +EaEQPkxROpaw0RPxx9gmrjrKkcRpnd8BKWRRb2jaFOwIQZeQjdCygPLPwj2/kWjFgGcexGATVdVh +mVd8upUPYUk6ynW8yQqTP2cOEvIo4jEbwFcW3wh8GcF+Dx+FHgo2fFt+J7x6v+Db9NpSvd4MVHAx +kUOVyLzwPt0JfjBkUO1/AaQzZ01oT74V77D2AhGiGxMlOtzCWfHjXEa7ZywCRuoeSKbmW9m1vFGi +kpbbqsY3Iqb+zCB0oy2pLmvLwIIRIbWTee5Ehr7XHuQe+w== +-----END CERTIFICATE----- + +WoSign China +============ +-----BEGIN CERTIFICATE----- +MIIFWDCCA0CgAwIBAgIQUHBrzdgT/BtOOzNy0hFIjTANBgkqhkiG9w0BAQsFADBGMQswCQYDVQQG +EwJDTjEaMBgGA1UEChMRV29TaWduIENBIExpbWl0ZWQxGzAZBgNVBAMMEkNBIOayg+mAmuagueiv +geS5pjAeFw0wOTA4MDgwMTAwMDFaFw0zOTA4MDgwMTAwMDFaMEYxCzAJBgNVBAYTAkNOMRowGAYD +VQQKExFXb1NpZ24gQ0EgTGltaXRlZDEbMBkGA1UEAwwSQ0Eg5rKD6YCa5qC56K+B5LmmMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA0EkhHiX8h8EqwqzbdoYGTufQdDTc7WU1/FDWiD+k +8H/rD195L4mx/bxjWDeTmzj4t1up+thxx7S8gJeNbEvxUNUqKaqoGXqW5pWOdO2XCld19AXbbQs5 +uQF/qvbW2mzmBeCkTVL829B0txGMe41P/4eDrv8FAxNXUDf+jJZSEExfv5RxadmWPgxDT74wwJ85 +dE8GRV2j1lY5aAfMh09Qd5Nx2UQIsYo06Yms25tO4dnkUkWMLhQfkWsZHWgpLFbE4h4TV2TwYeO5 +Ed+w4VegG63XX9Gv2ystP9Bojg/qnw+LNVgbExz03jWhCl3W6t8Sb8D7aQdGctyB9gQjF+BNdeFy +b7Ao65vh4YOhn0pdr8yb+gIgthhid5E7o9Vlrdx8kHccREGkSovrlXLp9glk3Kgtn3R46MGiCWOc +76DbT52VqyBPt7D3h1ymoOQ3OMdc4zUPLK2jgKLsLl3Az+2LBcLmc272idX10kaO6m1jGx6KyX2m ++Jzr5dVjhU1zZmkR/sgO9MHHZklTfuQZa/HpelmjbX7FF+Ynxu8b22/8DU0GAbQOXDBGVWCvOGU6 +yke6rCzMRh+yRpY/8+0mBe53oWprfi1tWFxK1I5nuPHa1UaKJ/kR8slC/k7e3x9cxKSGhxYzoacX +GKUN5AXlK8IrC6KVkLn9YDxOiT7nnO4fuwECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFOBNv9ybQV0T6GTwp+kVpOGBwboxMA0GCSqGSIb3DQEBCwUA +A4ICAQBqinA4WbbaixjIvirTthnVZil6Xc1bL3McJk6jfW+rtylNpumlEYOnOXOvEESS5iVdT2H6 +yAa+Tkvv/vMx/sZ8cApBWNromUuWyXi8mHwCKe0JgOYKOoICKuLJL8hWGSbueBwj/feTZU7n85iY +r83d2Z5AiDEoOqsuC7CsDCT6eiaY8xJhEPRdF/d+4niXVOKM6Cm6jBAyvd0zaziGfjk9DgNyp115 +j0WKWa5bIW4xRtVZjc8VX90xJc/bYNaBRHIpAlf2ltTW/+op2znFuCyKGo3Oy+dCMYYFaA6eFN0A +kLppRQjbbpCBhqcqBT/mhDn4t/lXX0ykeVoQDF7Va/81XwVRHmyjdanPUIPTfPRm94KNPQx96N97 +qA4bLJyuQHCH2u2nFoJavjVsIE4iYdm8UXrNemHcSxH5/mc0zy4EZmFcV5cjjPOGG0jfKq+nwf/Y +jj4Du9gqsPoUJbJRa4ZDhS4HIxaAjUz7tGM7zMN07RujHv41D198HRaG9Q7DlfEvr10lO1Hm13ZB +ONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Leie2uPAmvylezkolwQOQv +T8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR12KvxAmLBsX5VYc8T1yaw15zLKYs4SgsO +kI26oQ== +-----END CERTIFICATE----- From 4820d7318d302dfae5efbb6db3d24f804ab06778 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 5 Jan 2015 15:40:09 -0800 Subject: [PATCH 210/952] v3.22.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a4fb70f93..962d5cf7b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.22.1 2015-01-05 +================= +Updated cacert.pem + 3.22.0 2015-01-05 ================= Added JavaScript-based plugin support diff --git a/Gemfile.lock b/Gemfile.lock index fd10d5b70..c3f304cae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.22.0) + heroku (3.22.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3546aa2b1..a771f48c0 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.22.0" + VERSION = "3.22.1" end From c358d43e0f9421c5bda68c05304f7c221e23bf6b Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 5 Jan 2015 22:40:57 -0800 Subject: [PATCH 211/952] =?UTF-8?q?(>=E1=83=9A)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/heroku/helpers/heroku_postgresql.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index bd1f21866..a8103d471 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -197,7 +197,7 @@ def hpg_translate_db_opts_to_urls(addon, config) else attachment = resolver.resolve(val) if attachment.starter_plan? - error("#{opt.tr 'f', 'F'} is only available on production databases.") + error("#{opt.capitalize} is only available on production databases.") end argument_url = attachment.url end From 0e02788f3f0006c4e77cba35f70b365c464ded24 Mon Sep 17 00:00:00 2001 From: Jordan Curzon Date: Tue, 6 Jan 2015 10:14:05 -0800 Subject: [PATCH 212/952] add debug statements when logging fails --- lib/heroku/client.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 353aa5da9..0bc6618ed 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -480,6 +480,7 @@ def run_console_command(url, command, prefix=nil) def read_logs(app_name, options=[]) query = "&" + options.join("&") unless options.empty? url = get("/apps/#{app_name}/logs?logplex=true#{query}").to_s + debug "Reading logs from: #{url}" if url == 'Use old logs' puts get("/apps/#{app_name}/logs").to_s else @@ -526,7 +527,8 @@ def read_logs(app_name, options=[]) end end end - rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError + rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => exception + debug "Error connecting to logging service: #{exception}" error("Could not connect to logging service") rescue Timeout::Error, EOFError error("\nRequest timed out") From c6a91ef6338d6d4fe1c0f096e290b68bc2b88a24 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Wed, 7 Jan 2015 17:36:03 -0800 Subject: [PATCH 213/952] Extract OpenSSL commands to separate module --- lib/heroku/command/certs.rb | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 593c8618e..8c3e8ea26 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -160,16 +160,8 @@ def generate domain = args[0] || error("certs:generate must specify a domain") subject = args[1] - keyfile = "#{domain}.key" - csrfile = "#{domain}.csr" + keyfile, csrfile = OpenSSLTool.generate_csr(domain, subject) - subj_args = if subject - ['-subj', subject] - else - [] - end - - system("openssl", "req", "-new", "-newkey", "rsa:2048", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) display "Your CSR is in #{csrfile} and your key is in #{keyfile}." display "When you've received your certificate, run:" @@ -263,4 +255,21 @@ def all_endpoint_domains .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ .reduce(:+) end + + module OpenSSLTool + def self.generate_csr(domain, subject = nil, key_size = 2048) + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + subj_args = if subject + ['-subj', subject] + else + [] + end + + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) + + return [keyfile, csrfile] + end + end end From 3079be875bff1322079d52c3818b00fe678d2feb Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Wed, 7 Jan 2015 17:36:16 -0800 Subject: [PATCH 214/952] Tweak wording of instructions --- lib/heroku/command/certs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 8c3e8ea26..c9ed37af7 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -162,7 +162,7 @@ def generate keyfile, csrfile = OpenSSLTool.generate_csr(domain, subject) - display "Your CSR is in #{csrfile} and your key is in #{keyfile}." + display "Submit the CSR in #{csrfile} to your preferred certificate authority." display "When you've received your certificate, run:" if all_endpoint_domains.include? domain From 9a40fff0ae67ed0b385703839e0410bfa18297de Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Wed, 7 Jan 2015 18:09:12 -0800 Subject: [PATCH 215/952] Help the user if openssl(1) isn't installed And catch errors more generally. --- lib/heroku/command/certs.rb | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index c9ed37af7..cfb34cacf 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -170,6 +170,12 @@ def generate else display "$ heroku certs:add CERTFILE #{keyfile}" end + + rescue OpenSSLTool::NotInstalledError => ex + error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) + + rescue OpenSSLTool::GenericError => ex + error(ex.message) end private @@ -258,6 +264,8 @@ def all_endpoint_domains module OpenSSLTool def self.generate_csr(domain, subject = nil, key_size = 2048) + ensure_openssl_installed! + keyfile = "#{domain}.key" csrfile = "#{domain}.csr" @@ -267,9 +275,33 @@ def self.generate_csr(domain, subject = nil, key_size = 2048) [] end - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) or raise GenericError, "Key and CSR generation failed: #{$?}" return [keyfile, csrfile] end + + class GenericError < StandardError; end + + class NotInstalledError < GenericError + include Heroku::Helpers + + def installation_hint + if running_on_a_mac? + "With Homebrew installed, run the following command:\n$ brew install openssl" + elsif running_on_windows? + "Download and install OpenSSL from ." + else + # Probably some kind of Linux or other Unix. Who knows what package manager they're using? + "Make sure your package manager's 'openssl' package is installed." + end + end + end + + private + def self.ensure_openssl_installed! + return if @checked + system("openssl", "version") or raise NotInstalledError + @checked = true + end end end From 23433acb7c9cc3a187891cf4c5997459bfeb2371 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Wed, 7 Jan 2015 18:40:58 -0800 Subject: [PATCH 216/952] Move OpenSSLTool to its own file --- lib/heroku/command/certs.rb | 50 +++---------------------------------- lib/heroku/open_ssl_tool.rb | 46 ++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 46 deletions(-) create mode 100644 lib/heroku/open_ssl_tool.rb diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index cfb34cacf..ba081324d 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -1,4 +1,5 @@ require "heroku/command/base" +require "heroku/open_ssl_tool" require "excon" # manage ssl endpoints for an app @@ -160,7 +161,7 @@ def generate domain = args[0] || error("certs:generate must specify a domain") subject = args[1] - keyfile, csrfile = OpenSSLTool.generate_csr(domain, subject) + keyfile, csrfile = Heroku::OpenSSLTool.generate_csr(domain, subject) display "Submit the CSR in #{csrfile} to your preferred certificate authority." display "When you've received your certificate, run:" @@ -171,10 +172,10 @@ def generate display "$ heroku certs:add CERTFILE #{keyfile}" end - rescue OpenSSLTool::NotInstalledError => ex + rescue Heroku::OpenSSLTool::NotInstalledError => ex error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) - rescue OpenSSLTool::GenericError => ex + rescue Heroku::OpenSSLTool::GenericError => ex error(ex.message) end @@ -261,47 +262,4 @@ def all_endpoint_domains .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ .reduce(:+) end - - module OpenSSLTool - def self.generate_csr(domain, subject = nil, key_size = 2048) - ensure_openssl_installed! - - keyfile = "#{domain}.key" - csrfile = "#{domain}.csr" - - subj_args = if subject - ['-subj', subject] - else - [] - end - - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) or raise GenericError, "Key and CSR generation failed: #{$?}" - - return [keyfile, csrfile] - end - - class GenericError < StandardError; end - - class NotInstalledError < GenericError - include Heroku::Helpers - - def installation_hint - if running_on_a_mac? - "With Homebrew installed, run the following command:\n$ brew install openssl" - elsif running_on_windows? - "Download and install OpenSSL from ." - else - # Probably some kind of Linux or other Unix. Who knows what package manager they're using? - "Make sure your package manager's 'openssl' package is installed." - end - end - end - - private - def self.ensure_openssl_installed! - return if @checked - system("openssl", "version") or raise NotInstalledError - @checked = true - end - end end diff --git a/lib/heroku/open_ssl_tool.rb b/lib/heroku/open_ssl_tool.rb new file mode 100644 index 000000000..2ce47edac --- /dev/null +++ b/lib/heroku/open_ssl_tool.rb @@ -0,0 +1,46 @@ +require "heroku/helpers" + +module Heroku +module OpenSSLTool + def self.generate_csr(domain, subject = nil, key_size = 2048) + ensure_openssl_installed! + + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + subj_args = if subject + ['-subj', subject] + else + [] + end + + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) or raise GenericError, "Key and CSR generation failed: #{$?}" + + return [keyfile, csrfile] + end + + class GenericError < StandardError; end + + class NotInstalledError < GenericError + include Heroku::Helpers + + def installation_hint + if running_on_a_mac? + "With Homebrew installed, run the following command:\n$ brew install openssl" + elsif running_on_windows? + "Download and install OpenSSL from ." + else + # Probably some kind of Linux or other Unix. Who knows what package manager they're using? + "Make sure your package manager's 'openssl' package is installed." + end + end + end + +private + def self.ensure_openssl_installed! + return if @checked + system("openssl", "version") or raise NotInstalledError + @checked = true + end +end +end From 1a4b48220c51601a5daaacc0172b82f347bc4799 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Wed, 7 Jan 2015 18:42:52 -0800 Subject: [PATCH 217/952] Rename and reformat Heroku::OpenSSLTool --- lib/heroku/command/certs.rb | 8 +++---- lib/heroku/open_ssl.rb | 46 +++++++++++++++++++++++++++++++++++++ lib/heroku/open_ssl_tool.rb | 46 ------------------------------------- 3 files changed, 50 insertions(+), 50 deletions(-) create mode 100644 lib/heroku/open_ssl.rb delete mode 100644 lib/heroku/open_ssl_tool.rb diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index ba081324d..b50e5c01f 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -1,5 +1,5 @@ require "heroku/command/base" -require "heroku/open_ssl_tool" +require "heroku/open_ssl" require "excon" # manage ssl endpoints for an app @@ -161,7 +161,7 @@ def generate domain = args[0] || error("certs:generate must specify a domain") subject = args[1] - keyfile, csrfile = Heroku::OpenSSLTool.generate_csr(domain, subject) + keyfile, csrfile = Heroku::OpenSSL.generate_csr(domain, subject) display "Submit the CSR in #{csrfile} to your preferred certificate authority." display "When you've received your certificate, run:" @@ -172,10 +172,10 @@ def generate display "$ heroku certs:add CERTFILE #{keyfile}" end - rescue Heroku::OpenSSLTool::NotInstalledError => ex + rescue Heroku::OpenSSL::NotInstalledError => ex error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) - rescue Heroku::OpenSSLTool::GenericError => ex + rescue Heroku::OpenSSL::GenericError => ex error(ex.message) end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb new file mode 100644 index 000000000..7a157f2c0 --- /dev/null +++ b/lib/heroku/open_ssl.rb @@ -0,0 +1,46 @@ +require "heroku/helpers" + +module Heroku + module OpenSSL + def self.generate_csr(domain, subject = nil, key_size = 2048) + ensure_openssl_installed! + + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + subj_args = if subject + ['-subj', subject] + else + [] + end + + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) or raise GenericError, "Key and CSR generation failed: #{$?}" + + return [keyfile, csrfile] + end + + class GenericError < StandardError; end + + class NotInstalledError < GenericError + include Heroku::Helpers + + def installation_hint + if running_on_a_mac? + "With Homebrew installed, run the following command:\n$ brew install openssl" + elsif running_on_windows? + "Download and install OpenSSL from ." + else + # Probably some kind of Linux or other Unix. Who knows what package manager they're using? + "Make sure your package manager's 'openssl' package is installed." + end + end + end + + private + def self.ensure_openssl_installed! + return if @checked + system("openssl", "version") or raise NotInstalledError + @checked = true + end + end +end diff --git a/lib/heroku/open_ssl_tool.rb b/lib/heroku/open_ssl_tool.rb deleted file mode 100644 index 2ce47edac..000000000 --- a/lib/heroku/open_ssl_tool.rb +++ /dev/null @@ -1,46 +0,0 @@ -require "heroku/helpers" - -module Heroku -module OpenSSLTool - def self.generate_csr(domain, subject = nil, key_size = 2048) - ensure_openssl_installed! - - keyfile = "#{domain}.key" - csrfile = "#{domain}.csr" - - subj_args = if subject - ['-subj', subject] - else - [] - end - - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) or raise GenericError, "Key and CSR generation failed: #{$?}" - - return [keyfile, csrfile] - end - - class GenericError < StandardError; end - - class NotInstalledError < GenericError - include Heroku::Helpers - - def installation_hint - if running_on_a_mac? - "With Homebrew installed, run the following command:\n$ brew install openssl" - elsif running_on_windows? - "Download and install OpenSSL from ." - else - # Probably some kind of Linux or other Unix. Who knows what package manager they're using? - "Make sure your package manager's 'openssl' package is installed." - end - end - end - -private - def self.ensure_openssl_installed! - return if @checked - system("openssl", "version") or raise NotInstalledError - @checked = true - end -end -end From 7fd149fe4756bcdce679d71a58a901c537d21a0f Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 00:24:07 -0800 Subject: [PATCH 218/952] Construct subject line ourselves Since it's remarkably difficult to get a default domain in openssl(1)'s prompting. --- lib/heroku/command/certs.rb | 48 ++++++++++++++++++++++++++++++++++--- lib/heroku/open_ssl.rb | 17 +++++-------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index b50e5c01f..b3ef2f510 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -154,12 +154,22 @@ def rollback display_certificate_info(endpoint) end - # certs:generate DOMAIN [SUBJECT] + # certs:generate DOMAIN # - # Generate a certificate signing request and key for an app. + # Generate a certificate signing request and key for an app. Prompts for + # information to put in the certificate unless --now is used, or at least + # one of the --subject, --owner, --country, --area, or --city options + # is specified. + # + # --owner NAME # name of organization certificate belongs to + # --country COUNTRY # country of owner, as a two-letter ISO country code + # --area AREA # sub-counry area (state, province, etc.) of owner + # --city CITY # city of owner + # --subject SUBJECT # specify entire certificate subject + # --now # do not prompt for any owner information def generate domain = args[0] || error("certs:generate must specify a domain") - subject = args[1] + subject = cert_subject_for_domain_and_options(domain, options) keyfile, csrfile = Heroku::OpenSSL.generate_csr(domain, subject) @@ -262,4 +272,36 @@ def all_endpoint_domains .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ .reduce(:+) end + + def prompt(question) + display("#{question}: ", false) + ask + end + + def val_empty?(val) + val.nil? or val.empty? + end + + def cert_subject_for_domain_and_options(domain, options = {}) + subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now) + + if val_empty? subject + if !now && [country, area, city, owner].all? { |v| val_empty? v } + owner = prompt "Owner of this certificate" + country = prompt "Country of owner (two-letter ISO code)" + area = prompt "State/province/etc. of owner" + city = prompt "City of owner" + end + + subject = "" + subject += "/C=#{country}" unless val_empty? country + subject += "/ST=#{area}" unless val_empty? area + subject += "/L=#{city}" unless val_empty? city + subject += "/O=#{owner}" unless val_empty? owner + + subject += "/CN=#{domain}" + end + + subject + end end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index 7a157f2c0..e24b7c811 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -1,24 +1,19 @@ require "heroku/helpers" +require "tempfile" module Heroku module OpenSSL - def self.generate_csr(domain, subject = nil, key_size = 2048) + def self.generate_csr(domain, subject, key_size = 2048) ensure_openssl_installed! keyfile = "#{domain}.key" csrfile = "#{domain}.csr" - - subj_args = if subject - ['-subj', subject] - else - [] - end - - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, *subj_args) or raise GenericError, "Key and CSR generation failed: #{$?}" - + + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, "-subj", subject) or raise GenericError, "Key and CSR generation failed: #{$?}" + return [keyfile, csrfile] end - + class GenericError < StandardError; end class NotInstalledError < GenericError From 4f168a91ed671b01bdf3b26828eff1f26101c49c Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 00:48:53 -0800 Subject: [PATCH 219/952] Improve output, including handling uninstalled addon --- lib/heroku/command/certs.rb | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index b3ef2f510..668835e99 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -171,17 +171,23 @@ def generate domain = args[0] || error("certs:generate must specify a domain") subject = cert_subject_for_domain_and_options(domain, options) - keyfile, csrfile = Heroku::OpenSSL.generate_csr(domain, subject) + keyfile, csrfile, crtfile = Heroku::OpenSSL.generate_csr(domain, subject) - display "Submit the CSR in #{csrfile} to your preferred certificate authority." + display "Your key and certificate signing request have been generated." + display "Submit the CSR in '#{csrfile}' to your preferred certificate authority." display "When you've received your certificate, run:" - if all_endpoint_domains.include? domain - display "$ heroku certs:update CERTFILE #{keyfile}" - else - display "$ heroku certs:add CERTFILE #{keyfile}" + needs_addon = false + command = "add" + begin + command = "update" if all_endpoint_domains.include? domain + rescue RestClient::Forbidden + needs_addon = true end + display "$ heroku addons:add ssl:endpoint" if needs_addon + display "$ heroku certs:#{command} #{crtfile || "CERTFILE"} #{keyfile}" + rescue Heroku::OpenSSL::NotInstalledError => ex error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) From 3c40ef285fee09ee4db4b5086a9b5eea38c123f4 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 00:53:53 -0800 Subject: [PATCH 220/952] Support generating self-signed certificates --- lib/heroku/command/certs.rb | 26 ++++++++++++++++++-------- lib/heroku/open_ssl.rb | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 668835e99..8a7ceadc3 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -156,11 +156,12 @@ def rollback # certs:generate DOMAIN # - # Generate a certificate signing request and key for an app. Prompts for - # information to put in the certificate unless --now is used, or at least - # one of the --subject, --owner, --country, --area, or --city options - # is specified. + # Generate a key and certificate signing request (or self-signed certificate) + # for an app. Prompts for information to put in the certificate unless --now + # is used, or at least one of the --subject, --owner, --country, --area, or + # --city options is specified. # + # --selfsigned # generate a self-signed certificate instead of a CSR # --owner NAME # name of organization certificate belongs to # --country COUNTRY # country of owner, as a two-letter ISO country code # --area AREA # sub-counry area (state, province, etc.) of owner @@ -171,11 +172,20 @@ def generate domain = args[0] || error("certs:generate must specify a domain") subject = cert_subject_for_domain_and_options(domain, options) - keyfile, csrfile, crtfile = Heroku::OpenSSL.generate_csr(domain, subject) + keyfile, csrfile, crtfile = if options[:selfsigned] + Heroku::OpenSSL.generate_self_signed_certificate(domain, subject) + else + Heroku::OpenSSL.generate_csr(domain, subject) + end - display "Your key and certificate signing request have been generated." - display "Submit the CSR in '#{csrfile}' to your preferred certificate authority." - display "When you've received your certificate, run:" + if csrfile.nil? + display "Your key and self-signed certificate have been generated." + display "Next, run:" + else + display "Your key and certificate signing request have been generated." + display "Submit the CSR in '#{csrfile}' to your preferred certificate authority." + display "When you've received your certificate, run:" + end needs_addon = false command = "add" diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index e24b7c811..6a2fe6e62 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -4,15 +4,24 @@ module Heroku module OpenSSL def self.generate_csr(domain, subject, key_size = 2048) - ensure_openssl_installed! - keyfile = "#{domain}.key" csrfile = "#{domain}.csr" - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", csrfile, "-subj", subject) or raise GenericError, "Key and CSR generation failed: #{$?}" + openssl_req_new(keyfile, csrfile, subject, key_size) or raise GenericError, "Key and CSR generation failed: #{$?}" return [keyfile, csrfile] end + + def self.generate_self_signed_certificate(domain, subject, key_size = 2048) + ensure_openssl_installed! + + keyfile = "#{domain}.key" + crtfile = "#{domain}.crt" + + openssl_req_new(keyfile, crtfile, subject, key_size, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" + + return [keyfile, nil, crtfile] + end class GenericError < StandardError; end @@ -32,6 +41,11 @@ def installation_hint end private + def self.openssl_req_new(keyfile, outfile, subject, key_size, *args) + ensure_openssl_installed! + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) + end + def self.ensure_openssl_installed! return if @checked system("openssl", "version") or raise NotInstalledError From a2481a53044fd8237583c0565084e0efa1bc1259 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 01:28:13 -0800 Subject: [PATCH 221/952] Extract "next step" explainer to its own method --- lib/heroku/command/certs.rb | 43 +++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 8a7ceadc3..bbf7db192 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -178,26 +178,9 @@ def generate Heroku::OpenSSL.generate_csr(domain, subject) end - if csrfile.nil? - display "Your key and self-signed certificate have been generated." - display "Next, run:" - else - display "Your key and certificate signing request have been generated." - display "Submit the CSR in '#{csrfile}' to your preferred certificate authority." - display "When you've received your certificate, run:" - end - - needs_addon = false - command = "add" - begin - command = "update" if all_endpoint_domains.include? domain - rescue RestClient::Forbidden - needs_addon = true - end - - display "$ heroku addons:add ssl:endpoint" if needs_addon - display "$ heroku certs:#{command} #{crtfile || "CERTFILE"} #{keyfile}" + explain_step_after_generate(keyfile, csrfile, crtfile) + rescue Heroku::OpenSSL::NotInstalledError => ex error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) @@ -320,4 +303,26 @@ def cert_subject_for_domain_and_options(domain, options = {}) subject end + + def explain_step_after_generate(keyfile, csrfile, crtfile) + if csrfile.nil? + display "Your key and self-signed certificate have been generated." + display "Next, run:" + else + display "Your key and certificate signing request have been generated." + display "Submit the CSR in '#{csrfile}' to your preferred certificate authority." + display "When you've received your certificate, run:" + end + + needs_addon = false + command = "add" + begin + command = "update" if all_endpoint_domains.include? domain + rescue RestClient::Forbidden + needs_addon = true + end + + display "$ heroku addons:add ssl:endpoint" if needs_addon + display "$ heroku certs:#{command} #{crtfile || "CERTFILE"} #{keyfile}" + end end From 4418a293d7b623b966b04d41c3bcf35e135ce016 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 01:30:37 -0800 Subject: [PATCH 222/952] Make object for each certificate request We're just getting too many parameters to methods. --- lib/heroku/command/certs.rb | 12 +++---- lib/heroku/open_ssl.rb | 62 +++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 30 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index bbf7db192..1aa4cbf18 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -169,15 +169,13 @@ def rollback # --subject SUBJECT # specify entire certificate subject # --now # do not prompt for any owner information def generate - domain = args[0] || error("certs:generate must specify a domain") - subject = cert_subject_for_domain_and_options(domain, options) + request = Heroku::OpenSSL::CertificateRequest.new - keyfile, csrfile, crtfile = if options[:selfsigned] - Heroku::OpenSSL.generate_self_signed_certificate(domain, subject) - else - Heroku::OpenSSL.generate_csr(domain, subject) - end + request.domain = args[0] || error("certs:generate must specify a domain") + request.subject = cert_subject_for_domain_and_options(request.domain, options) + request.self_signed = options[:selfsigned] || false + keyfile, csrfile, crtfile = request.generate explain_step_after_generate(keyfile, csrfile, crtfile) diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index 6a2fe6e62..cd2766b45 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -3,31 +3,53 @@ module Heroku module OpenSSL - def self.generate_csr(domain, subject, key_size = 2048) - keyfile = "#{domain}.key" - csrfile = "#{domain}.csr" + class CertificateRequest + attr_accessor :domain, :subject, :key_size, :self_signed - openssl_req_new(keyfile, csrfile, subject, key_size) or raise GenericError, "Key and CSR generation failed: #{$?}" + def initialize() + @key_size = 2048 + @self_signed = false + super + end - return [keyfile, csrfile] - end - - def self.generate_self_signed_certificate(domain, subject, key_size = 2048) - ensure_openssl_installed! + def generate + if self_signed + generate_self_signed_certificate + else + generate_csr + end + end + + def generate_csr + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" + + return [keyfile, csrfile] + end - keyfile = "#{domain}.key" - crtfile = "#{domain}.crt" + def generate_self_signed_certificate + keyfile = "#{domain}.key" + crtfile = "#{domain}.crt" - openssl_req_new(keyfile, crtfile, subject, key_size, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" + openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" - return [keyfile, nil, crtfile] - end + return [keyfile, nil, crtfile] + end + private + def openssl_req_new(keyfile, outfile, *args) + Heroku::OpenSSL.ensure_openssl_installed! + system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) + end + end + class GenericError < StandardError; end - + class NotInstalledError < GenericError include Heroku::Helpers - + def installation_hint if running_on_a_mac? "With Homebrew installed, run the following command:\n$ brew install openssl" @@ -39,13 +61,7 @@ def installation_hint end end end - - private - def self.openssl_req_new(keyfile, outfile, subject, key_size, *args) - ensure_openssl_installed! - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) - end - + def self.ensure_openssl_installed! return if @checked system("openssl", "version") or raise NotInstalledError From 9005c075cbe236cf3bc147312b572a360c306683 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 01:34:31 -0800 Subject: [PATCH 223/952] Support changing key size --- lib/heroku/command/certs.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 1aa4cbf18..cbb3da0a5 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -162,6 +162,7 @@ def rollback # --city options is specified. # # --selfsigned # generate a self-signed certificate instead of a CSR + # --keysize BITSIZE # RSA key size in bits (default: 2048) # --owner NAME # name of organization certificate belongs to # --country COUNTRY # country of owner, as a two-letter ISO country code # --area AREA # sub-counry area (state, province, etc.) of owner @@ -174,6 +175,7 @@ def generate request.domain = args[0] || error("certs:generate must specify a domain") request.subject = cert_subject_for_domain_and_options(request.domain, options) request.self_signed = options[:selfsigned] || false + request.key_size = (options[:keysize] || request.key_size).to_i keyfile, csrfile, crtfile = request.generate From 582c7b058b06c7204b371fd742e88b8991ad4514 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 01:43:16 -0800 Subject: [PATCH 224/952] Wrap request results in a result object --- lib/heroku/command/certs.rb | 12 ++++++------ lib/heroku/open_ssl.rb | 22 +++++++++++++++------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index cbb3da0a5..e308d34bf 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -177,9 +177,9 @@ def generate request.self_signed = options[:selfsigned] || false request.key_size = (options[:keysize] || request.key_size).to_i - keyfile, csrfile, crtfile = request.generate + result = request.generate - explain_step_after_generate(keyfile, csrfile, crtfile) + explain_step_after_generate result rescue Heroku::OpenSSL::NotInstalledError => ex error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) @@ -304,13 +304,13 @@ def cert_subject_for_domain_and_options(domain, options = {}) subject end - def explain_step_after_generate(keyfile, csrfile, crtfile) - if csrfile.nil? + def explain_step_after_generate(result) + if result.csr_file.nil? display "Your key and self-signed certificate have been generated." display "Next, run:" else display "Your key and certificate signing request have been generated." - display "Submit the CSR in '#{csrfile}' to your preferred certificate authority." + display "Submit the CSR in '#{result.csr_file}' to your preferred certificate authority." display "When you've received your certificate, run:" end @@ -323,6 +323,6 @@ def explain_step_after_generate(keyfile, csrfile, crtfile) end display "$ heroku addons:add ssl:endpoint" if needs_addon - display "$ heroku certs:#{command} #{crtfile || "CERTFILE"} #{keyfile}" + display "$ heroku certs:#{command} #{result.crt_file || "CERTFILE"} #{result.key_file}" end end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index cd2766b45..309ef6297 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -20,25 +20,33 @@ def generate end end + class Result + attr_accessor :key_file, :csr_file, :crt_file + + def initialize(key_file, csr_file, crt_file) + @key_file, @csr_file, @crt_file = key_file, csr_file, crt_file + end + end + + private def generate_csr keyfile = "#{domain}.key" csrfile = "#{domain}.csr" - + openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" - - return [keyfile, csrfile] + + return Result.new(keyfile, csrfile, nil) end def generate_self_signed_certificate keyfile = "#{domain}.key" crtfile = "#{domain}.crt" - + openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" - - return [keyfile, nil, crtfile] + + return Result.new(keyfile, nil, crtfile) end - private def openssl_req_new(keyfile, outfile, *args) Heroku::OpenSSL.ensure_openssl_installed! system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) From 0d22e1575f9cbcf44a5001c2cf2155bb0dc311df Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 01:48:05 -0800 Subject: [PATCH 225/952] Fix bug in explain_step_after_generate Contained a reference to a variable that no longer exists. --- lib/heroku/command/certs.rb | 2 +- lib/heroku/open_ssl.rb | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index e308d34bf..c582cfc47 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -317,7 +317,7 @@ def explain_step_after_generate(result) needs_addon = false command = "add" begin - command = "update" if all_endpoint_domains.include? domain + command = "update" if all_endpoint_domains.include? result.request.domain rescue RestClient::Forbidden needs_addon = true end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index 309ef6297..90b6f0f7f 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -21,9 +21,10 @@ def generate end class Result - attr_accessor :key_file, :csr_file, :crt_file + attr_accessor :request, :key_file, :csr_file, :crt_file - def initialize(key_file, csr_file, crt_file) + def initialize(request, key_file, csr_file, crt_file) + @request = request.dup @key_file, @csr_file, @crt_file = key_file, csr_file, crt_file end end @@ -35,7 +36,7 @@ def generate_csr openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" - return Result.new(keyfile, csrfile, nil) + return Result.new(self, keyfile, csrfile, nil) end def generate_self_signed_certificate @@ -44,7 +45,7 @@ def generate_self_signed_certificate openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" - return Result.new(keyfile, nil, crtfile) + return Result.new(self, keyfile, nil, crtfile) end def openssl_req_new(keyfile, outfile, *args) From 59bb6a55957ad0e69a92ec10bb3bdd1b47888c2a Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 01:52:44 -0800 Subject: [PATCH 226/952] Correct typo in usage text --- lib/heroku/command/certs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index c582cfc47..930744978 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -165,7 +165,7 @@ def rollback # --keysize BITSIZE # RSA key size in bits (default: 2048) # --owner NAME # name of organization certificate belongs to # --country COUNTRY # country of owner, as a two-letter ISO country code - # --area AREA # sub-counry area (state, province, etc.) of owner + # --area AREA # sub-country area (state, province, etc.) of owner # --city CITY # city of owner # --subject SUBJECT # specify entire certificate subject # --now # do not prompt for any owner information From b054410e0d34a512845857b365821e5db50aa3df Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 18:57:05 -0800 Subject: [PATCH 227/952] Let user set command through OPENSSL env var And add the first tests for all this stuff. --- lib/heroku/open_ssl.rb | 16 ++++++++++++++-- spec/heroku/open_ssl_spec.rb | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 spec/heroku/open_ssl_spec.rb diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index 90b6f0f7f..99d9c161f 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -3,6 +3,18 @@ module Heroku module OpenSSL + def self.openssl(*args) + if args.empty? + ENV["OPENSSL"] || "openssl" + else + system(openssl, *args) + end + end + + def self.openssl=(val) + ENV["OPENSSL"] = val + end + class CertificateRequest attr_accessor :domain, :subject, :key_size, :self_signed @@ -50,7 +62,7 @@ def generate_self_signed_certificate def openssl_req_new(keyfile, outfile, *args) Heroku::OpenSSL.ensure_openssl_installed! - system("openssl", "req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) + Heroku::OpenSSL.openssl("req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) end end @@ -73,7 +85,7 @@ def installation_hint def self.ensure_openssl_installed! return if @checked - system("openssl", "version") or raise NotInstalledError + openssl("version") or raise NotInstalledError @checked = true end end diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb new file mode 100644 index 000000000..edfc94f31 --- /dev/null +++ b/spec/heroku/open_ssl_spec.rb @@ -0,0 +1,26 @@ +require "heroku/open_ssl" + +describe Heroku::OpenSSL do + describe :openssl do + it "returns 'openssl' when nothing else is set" do + expect(Heroku::OpenSSL.openssl).to eq("openssl") + end + + it "returns the environment's 'OPENSSL' variable when it's set" do + ENV['OPENSSL'] = '/usr/bin/openssl' + expect(Heroku::OpenSSL.openssl).to eq('/usr/bin/openssl') + ENV['OPENSSL'] = nil + end + + it "can be set with openssl=" do + Heroku::OpenSSL.openssl = '/usr/local/bin/openssl' + expect(Heroku::OpenSSL.openssl).to eq('/usr/local/bin/openssl') + Heroku::OpenSSL.openssl = nil + end + + it "runs openssl(1) when passed arguments" do + expect(Heroku::OpenSSL).to receive(:system).with("openssl", "version").and_return(true) + expect(Heroku::OpenSSL.openssl("version")).to be true + end + end +end \ No newline at end of file From b4a903f3485fa31f1a6085a4e52b4cbf8139077c Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 19:14:12 -0800 Subject: [PATCH 228/952] Test OpenSSL installation check --- lib/heroku/open_ssl.rb | 1 + spec/heroku/open_ssl_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index 99d9c161f..e1941be6c 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -12,6 +12,7 @@ def self.openssl(*args) end def self.openssl=(val) + @checked = false ENV["OPENSSL"] = val end diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index edfc94f31..50b025b6f 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -23,4 +23,21 @@ expect(Heroku::OpenSSL.openssl("version")).to be true end end + + describe :ensure_openssl_installed! do + it "calls openssl(1) to ensure it's available" do + expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) + Heroku::OpenSSL.ensure_openssl_installed! + end + + it "detects openssl(1) is available when it is available" do + expect { Heroku::OpenSSL.ensure_openssl_installed! }.not_to raise_error + end + + it "detects openssl(1) is absent when it isn't available" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) + Heroku::OpenSSL.openssl = nil + end + end end \ No newline at end of file From 1e2a00021aeda33c0cf09b7d89af33d21bb468bf Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 19:19:54 -0800 Subject: [PATCH 229/952] Test installation hints on various OSes --- spec/heroku/open_ssl_spec.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 50b025b6f..b9ec712ec 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -39,5 +39,34 @@ expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) Heroku::OpenSSL.openssl = nil end + + it "gives good installation advice on a Mac" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + expect(ex).to receive(:running_on_a_mac?).and_return(true) + expect(ex.installation_hint).to match(/brew install openssl/) + } + Heroku::OpenSSL.openssl = nil + end + + it "gives good installation advice on Windows" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + expect(ex).to receive(:running_on_a_mac?).and_return(false) + expect(ex).to receive(:running_on_windows?).and_return(true) + expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) + } + Heroku::OpenSSL.openssl = nil + end + + it "gives good installation advice on miscellaneous Unixen" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + expect(ex).to receive(:running_on_a_mac?).and_return(false) + expect(ex).to receive(:running_on_windows?).and_return(false) + expect(ex.installation_hint).to match(/'openssl' package/) + } + Heroku::OpenSSL.openssl = nil + end end end \ No newline at end of file From 76ef4d86654978b103082265fd6514eb0b8f3ad0 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 19:24:35 -0800 Subject: [PATCH 230/952] Loosen the OS testing requirements a bit Doesn't hardcode the current implementation quite so much. --- spec/heroku/open_ssl_spec.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index b9ec712ec..7bc51a723 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -43,7 +43,8 @@ it "gives good installation advice on a Mac" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| - expect(ex).to receive(:running_on_a_mac?).and_return(true) + allow(ex).to receive(:running_on_a_mac?).and_return(true) + allow(ex).to receive(:running_on_windows?).and_return(false) expect(ex.installation_hint).to match(/brew install openssl/) } Heroku::OpenSSL.openssl = nil @@ -52,8 +53,8 @@ it "gives good installation advice on Windows" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| - expect(ex).to receive(:running_on_a_mac?).and_return(false) - expect(ex).to receive(:running_on_windows?).and_return(true) + allow(ex).to receive(:running_on_a_mac?).and_return(false) + allow(ex).to receive(:running_on_windows?).and_return(true) expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) } Heroku::OpenSSL.openssl = nil @@ -62,8 +63,8 @@ it "gives good installation advice on miscellaneous Unixen" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| - expect(ex).to receive(:running_on_a_mac?).and_return(false) - expect(ex).to receive(:running_on_windows?).and_return(false) + allow(ex).to receive(:running_on_a_mac?).and_return(false) + allow(ex).to receive(:running_on_windows?).and_return(false) expect(ex.installation_hint).to match(/'openssl' package/) } Heroku::OpenSSL.openssl = nil From cc4a6094ecdb82521fb2a6fa0962580d058e3771 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 20:15:01 -0800 Subject: [PATCH 231/952] Basic CertificateRequest tests --- spec/heroku/open_ssl_spec.rb | 67 ++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 7bc51a723..4e7d32a7a 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -70,4 +70,71 @@ Heroku::OpenSSL.openssl = nil end end + + describe :CertificateRequest do + it "initializes with good defaults" do + request = Heroku::OpenSSL::CertificateRequest.new + expect(request).not_to be_nil + expect(request.key_size).to eq(2048) + expect(request.self_signed).to be false + end + + it "creates a key and CSR when self_signed is off" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + result = request.generate + expect(result).not_to be_nil + expect(result.key_file).to eq('example.com.key') + expect(result.csr_file).to eq('example.com.csr') + expect(result.crt_file).to be_nil + + expect(File.read(result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") + expect(File.read(result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") + end + end + end + + it "creates a key and certificate when self_signed is on" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + request.self_signed = true + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + result = request.generate + expect(result).not_to be_nil + expect(result.key_file).to eq('example.com.key') + expect(result.csr_file).to be_nil + expect(result.crt_file).to eq('example.com.crt') + + expect(File.read(result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") + expect(File.read(result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") + end + end + end + + it "uses key_size to control the key's size" do + skip "Can't be tested without an rspec supporting to_stdout_from_any_process" unless RSpec::Matchers::BuiltIn::Output.method_defined? :to_stdout_from_any_process + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + request.key_size = 4096 + + expect { result = request.generate }.to output(/Generating a 4096 bit RSA private key/).to_stdout_from_any_process + end + end + end + end end \ No newline at end of file From 2f0d4d6c81f429238edf015c950651bceb31d4e7 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 20:33:12 -0800 Subject: [PATCH 232/952] Separate out different individual tests --- spec/heroku/open_ssl_spec.rb | 124 ++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 4e7d32a7a..c21b2ffd3 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -79,46 +79,94 @@ expect(request.self_signed).to be false end - it "creates a key and CSR when self_signed is off" do - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - - # Would like to do this, but the current version of rspec doesn't support it - # expect { result = request.generate }.to output.to_stdout_from_any_process - result = request.generate - expect(result).not_to be_nil - expect(result.key_file).to eq('example.com.key') - expect(result.csr_file).to eq('example.com.csr') - expect(result.crt_file).to be_nil - - expect(File.read(result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") - expect(File.read(result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") - end + context "generating with self_signed off" do + before(:all) do + @prev_dir = Dir.getwd + @dir = Dir.mktmpdir + Dir.chdir @dir + + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + @result = request.generate + end + + it "should create Result object" do + expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result + end + + it "should have a key filename" do + expect(@result.key_file).to eq('example.com.key') + end + + it "should have a CSR filename" do + expect(@result.csr_file).to eq('example.com.csr') + end + + it "should not have a certificate filename" do + expect(@result.crt_file).to be_nil + end + + it "should produce a PEM key file" do + expect(File.read(@result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") + end + + it "should produce a PEM certificate file" do + expect(File.read(@result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") + end + + after(:all) do + Dir.chdir @prev_dir + FileUtils.remove_entry_secure @dir end end - it "creates a key and certificate when self_signed is on" do - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - request.self_signed = true - - # Would like to do this, but the current version of rspec doesn't support it - # expect { result = request.generate }.to output.to_stdout_from_any_process - result = request.generate - expect(result).not_to be_nil - expect(result.key_file).to eq('example.com.key') - expect(result.csr_file).to be_nil - expect(result.crt_file).to eq('example.com.crt') - - expect(File.read(result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") - expect(File.read(result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") - end + context "generating with self_signed on" do + before(:all) do + @prev_dir = Dir.getwd + @dir = Dir.mktmpdir + Dir.chdir @dir + + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + request.self_signed = true + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + @result = request.generate + end + + it "should create Result object" do + expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result + end + + it "should have a key filename" do + expect(@result.key_file).to eq('example.com.key') + end + + it "should not have a CSR filename" do + expect(@result.csr_file).to be_nil + end + + it "should have a certificate filename" do + expect(@result.crt_file).to eq('example.com.crt') + end + + it "should produce a PEM key file" do + expect(File.read(@result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") + end + + it "should produce a PEM certificate file" do + expect(File.read(@result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") + end + + after(:all) do + Dir.chdir @prev_dir + FileUtils.remove_entry_secure @dir end end @@ -137,4 +185,4 @@ end end end -end \ No newline at end of file +end From c526f707dedae1c2307aea5bfddc47216718203d Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 20:36:53 -0800 Subject: [PATCH 233/952] Test that generating can raise an install error --- spec/heroku/open_ssl_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index c21b2ffd3..75c4fdfdd 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -170,6 +170,22 @@ end end + it "raises installation error when openssl(1) isn't installed" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + expect { result = request.generate }.to raise_error(Heroku::OpenSSL::NotInstalledError) + end + end + + Heroku::OpenSSL.openssl = nil + end + it "uses key_size to control the key's size" do skip "Can't be tested without an rspec supporting to_stdout_from_any_process" unless RSpec::Matchers::BuiltIn::Output.method_defined? :to_stdout_from_any_process From b756fe0be19671d0436cd8a2266bba0094021689 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Thu, 8 Jan 2015 20:48:28 -0800 Subject: [PATCH 234/952] Loosen key file format spec slightly Some versions of OpenSSL, including the version on Travis, leave out the "RSA". --- spec/heroku/open_ssl_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 75c4fdfdd..46cd6cd24 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -111,7 +111,7 @@ end it "should produce a PEM key file" do - expect(File.read(@result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") + expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) end it "should produce a PEM certificate file" do @@ -157,7 +157,7 @@ end it "should produce a PEM key file" do - expect(File.read(@result.key_file)).to start_with("-----BEGIN RSA PRIVATE KEY-----\n") + expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) end it "should produce a PEM certificate file" do From bef2c506e4ad6b72a10c07a4ad4e93fae6cb2cbe Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Fri, 9 Jan 2015 00:06:34 -0800 Subject: [PATCH 235/952] Add certs:generate tests --- spec/heroku/command/certs_spec.rb | 143 ++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/spec/heroku/command/certs_spec.rb b/spec/heroku/command/certs_spec.rb index 0c26d50ff..a5c36fca8 100644 --- a/spec/heroku/command/certs_spec.rb +++ b/spec/heroku/command/certs_spec.rb @@ -239,5 +239,148 @@ module Heroku::Command STDERR end end + + describe "certs:generate" do + context "fails early" do + it "if domain not specified" do + stdout, stderr = execute("certs:generate") + expect(stdout).to eq(" ! certs:generate must specify a domain\n") + end + end + + context "successfully" do + let(:request) do + request = Heroku::OpenSSL::CertificateRequest.new + expect(Heroku::OpenSSL::CertificateRequest).to receive(:new).and_return(request) + + expect(request).to receive(:generate) do + if request.self_signed + Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', nil, 'crtfile') + else + Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', 'csrfile', nil) + end + end + + request + end + + before(:each) do + stub_core.ssl_endpoint_list("example").returns([endpoint]) + request() + end + + describe "with subject prompts" do + it "emitted if no parts of subject provided" do + expect_prompts /Owner/ => "Heroku", /Country/ => 'US', /State/ => 'California', /City/ => 'San Francisco' + stub_core.ssl_endpoint_list("example").returns([endpoint]) + + stdout, stderr = execute("certs:generate example.com") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/C=US/ST=California/L=San Francisco/O=Heroku/CN=example.com") + end + + it "not emitted if any part of subject is specified" do + expect_prompts() + stub_core.ssl_endpoint_list("example").returns([endpoint]) + + stdout, stderr = execute("certs:generate example.com --owner Heroku") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/O=Heroku/CN=example.com") + end + + it "not emitted if --now is specified" do + expect_prompts() + + stdout, stderr = execute("certs:generate example.com --now") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/CN=example.com") + end + + it "not emitted if --subject is specified" do + expect_prompts() + + stdout, stderr = execute("certs:generate example.com --subject SOMETHING") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("SOMETHING") + end + + def expect_prompts(hash = {}) + hash.each do |question, answer| + expect_any_instance_of(Heroku::Command::Certs).to receive(:prompt).with(question).and_return(answer) + end + expect_any_instance_of(Heroku::Command::Certs).not_to receive(:prompt) + end + end + + describe "without --selfsigned" do + it "does not request a self-signed certificate" do + execute("certs:generate example.com --now") + expect(request.self_signed).to be false + end + + it "says it generated a key and CSR" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^Your key and certificate signing request have been generated.$/) + end + + it "says the name of the CSR file" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^Submit the CSR in 'csrfile' to your preferred certificate authority.$/) + end + end + + describe "with --selfsigned" do + it "requests a self-signed certificate" do + execute("certs:generate example.com --selfsigned --now") + expect(request.self_signed).to be true + end + + it "says it generated a key and self-signed certificate" do + stdout, stderr = execute("certs:generate example.com --selfsigned --now") + expect(stderr).to match(/^Your key and self-signed certificate have been generated.$/) + end + + it "says the name of the certificate file in the command" do + stdout, stderr = execute("certs:generate example.com --selfsigned --now") + expect(stderr).to match(/crtfile keyfile$/) + end + end + + describe "suggests next step" do + it "should be certs:add when domain is new" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) + end + + it "should be certs:update when domain is known" do + stdout, stderr = execute("certs:generate example.org --now") + expect(stderr).to match(/^\$ heroku certs:update CERTFILE keyfile$/) + end + + it "should be addons:add and certs:add when app doesn't have ssl:endpoint" do + stub_core.ssl_endpoint_list("example") { raise RestClient::Forbidden } + stdout, stderr = execute("certs:generate example.org --now") + expect(stderr).to match(/^\$ heroku addons:add ssl:endpoint$/) + expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) + end + end + + describe "key size" do + it "is 2048 unless otherwise specified" do + execute("certs:generate example.com --now") + expect(request.key_size).to eq(2048) + end + + it "can be changed using --keysize" do + execute("certs:generate example.com --now --keysize 4096") + expect(request.key_size).to eq(4096) + end + end + end + end end end From 2498968e7b16557c0fa2ce2ad349e57c094b6437 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Fri, 9 Jan 2015 00:07:42 -0800 Subject: [PATCH 236/952] Add domain sanity check --- lib/heroku/command/certs.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 930744978..412e9e116 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -282,6 +282,8 @@ def val_empty?(val) end def cert_subject_for_domain_and_options(domain, options = {}) + raise ArgumentError, "domain cannot be empty" if domain.nil? || domain.empty? + subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now) if val_empty? subject @@ -297,7 +299,7 @@ def cert_subject_for_domain_and_options(domain, options = {}) subject += "/ST=#{area}" unless val_empty? area subject += "/L=#{city}" unless val_empty? city subject += "/O=#{owner}" unless val_empty? owner - + subject += "/CN=#{domain}" end From 70a9eafe615fba8e57501836a47126108268a5b4 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 9 Jan 2015 17:59:55 -0800 Subject: [PATCH 237/952] Revert "Add certs:generate command" --- lib/heroku/command/certs.rb | 101 +-------------- lib/heroku/open_ssl.rb | 93 -------------- spec/heroku/command/certs_spec.rb | 143 --------------------- spec/heroku/open_ssl_spec.rb | 204 ------------------------------ 4 files changed, 2 insertions(+), 539 deletions(-) delete mode 100644 lib/heroku/open_ssl.rb delete mode 100644 spec/heroku/open_ssl_spec.rb diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 412e9e116..894091e06 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -1,5 +1,4 @@ require "heroku/command/base" -require "heroku/open_ssl" require "excon" # manage ssl endpoints for an app @@ -153,41 +152,7 @@ def rollback display "New active certificate details:" display_certificate_info(endpoint) end - - # certs:generate DOMAIN - # - # Generate a key and certificate signing request (or self-signed certificate) - # for an app. Prompts for information to put in the certificate unless --now - # is used, or at least one of the --subject, --owner, --country, --area, or - # --city options is specified. - # - # --selfsigned # generate a self-signed certificate instead of a CSR - # --keysize BITSIZE # RSA key size in bits (default: 2048) - # --owner NAME # name of organization certificate belongs to - # --country COUNTRY # country of owner, as a two-letter ISO country code - # --area AREA # sub-country area (state, province, etc.) of owner - # --city CITY # city of owner - # --subject SUBJECT # specify entire certificate subject - # --now # do not prompt for any owner information - def generate - request = Heroku::OpenSSL::CertificateRequest.new - - request.domain = args[0] || error("certs:generate must specify a domain") - request.subject = cert_subject_for_domain_and_options(request.domain, options) - request.self_signed = options[:selfsigned] || false - request.key_size = (options[:keysize] || request.key_size).to_i - - result = request.generate - - explain_step_after_generate result - - rescue Heroku::OpenSSL::NotInstalledError => ex - error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) - - rescue Heroku::OpenSSL::GenericError => ex - error(ex.message) - end - + private def current_endpoint @@ -264,67 +229,5 @@ def read_crt_and_key_bypassing_ssl_doctor def read_crt_and_key options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor end - - def all_endpoint_domains - endpoints = heroku.ssl_endpoint_list(app) - endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } \ - .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ - .reduce(:+) - end - - def prompt(question) - display("#{question}: ", false) - ask - end - - def val_empty?(val) - val.nil? or val.empty? - end - - def cert_subject_for_domain_and_options(domain, options = {}) - raise ArgumentError, "domain cannot be empty" if domain.nil? || domain.empty? - - subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now) - - if val_empty? subject - if !now && [country, area, city, owner].all? { |v| val_empty? v } - owner = prompt "Owner of this certificate" - country = prompt "Country of owner (two-letter ISO code)" - area = prompt "State/province/etc. of owner" - city = prompt "City of owner" - end - - subject = "" - subject += "/C=#{country}" unless val_empty? country - subject += "/ST=#{area}" unless val_empty? area - subject += "/L=#{city}" unless val_empty? city - subject += "/O=#{owner}" unless val_empty? owner - - subject += "/CN=#{domain}" - end - - subject - end - - def explain_step_after_generate(result) - if result.csr_file.nil? - display "Your key and self-signed certificate have been generated." - display "Next, run:" - else - display "Your key and certificate signing request have been generated." - display "Submit the CSR in '#{result.csr_file}' to your preferred certificate authority." - display "When you've received your certificate, run:" - end - - needs_addon = false - command = "add" - begin - command = "update" if all_endpoint_domains.include? result.request.domain - rescue RestClient::Forbidden - needs_addon = true - end - - display "$ heroku addons:add ssl:endpoint" if needs_addon - display "$ heroku certs:#{command} #{result.crt_file || "CERTFILE"} #{result.key_file}" - end + end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb deleted file mode 100644 index e1941be6c..000000000 --- a/lib/heroku/open_ssl.rb +++ /dev/null @@ -1,93 +0,0 @@ -require "heroku/helpers" -require "tempfile" - -module Heroku - module OpenSSL - def self.openssl(*args) - if args.empty? - ENV["OPENSSL"] || "openssl" - else - system(openssl, *args) - end - end - - def self.openssl=(val) - @checked = false - ENV["OPENSSL"] = val - end - - class CertificateRequest - attr_accessor :domain, :subject, :key_size, :self_signed - - def initialize() - @key_size = 2048 - @self_signed = false - super - end - - def generate - if self_signed - generate_self_signed_certificate - else - generate_csr - end - end - - class Result - attr_accessor :request, :key_file, :csr_file, :crt_file - - def initialize(request, key_file, csr_file, crt_file) - @request = request.dup - @key_file, @csr_file, @crt_file = key_file, csr_file, crt_file - end - end - - private - def generate_csr - keyfile = "#{domain}.key" - csrfile = "#{domain}.csr" - - openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" - - return Result.new(self, keyfile, csrfile, nil) - end - - def generate_self_signed_certificate - keyfile = "#{domain}.key" - crtfile = "#{domain}.crt" - - openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" - - return Result.new(self, keyfile, nil, crtfile) - end - - def openssl_req_new(keyfile, outfile, *args) - Heroku::OpenSSL.ensure_openssl_installed! - Heroku::OpenSSL.openssl("req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) - end - end - - class GenericError < StandardError; end - - class NotInstalledError < GenericError - include Heroku::Helpers - - def installation_hint - if running_on_a_mac? - "With Homebrew installed, run the following command:\n$ brew install openssl" - elsif running_on_windows? - "Download and install OpenSSL from ." - else - # Probably some kind of Linux or other Unix. Who knows what package manager they're using? - "Make sure your package manager's 'openssl' package is installed." - end - end - end - - def self.ensure_openssl_installed! - return if @checked - openssl("version") or raise NotInstalledError - @checked = true - end - end -end diff --git a/spec/heroku/command/certs_spec.rb b/spec/heroku/command/certs_spec.rb index a5c36fca8..0c26d50ff 100644 --- a/spec/heroku/command/certs_spec.rb +++ b/spec/heroku/command/certs_spec.rb @@ -239,148 +239,5 @@ module Heroku::Command STDERR end end - - describe "certs:generate" do - context "fails early" do - it "if domain not specified" do - stdout, stderr = execute("certs:generate") - expect(stdout).to eq(" ! certs:generate must specify a domain\n") - end - end - - context "successfully" do - let(:request) do - request = Heroku::OpenSSL::CertificateRequest.new - expect(Heroku::OpenSSL::CertificateRequest).to receive(:new).and_return(request) - - expect(request).to receive(:generate) do - if request.self_signed - Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', nil, 'crtfile') - else - Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', 'csrfile', nil) - end - end - - request - end - - before(:each) do - stub_core.ssl_endpoint_list("example").returns([endpoint]) - request() - end - - describe "with subject prompts" do - it "emitted if no parts of subject provided" do - expect_prompts /Owner/ => "Heroku", /Country/ => 'US', /State/ => 'California', /City/ => 'San Francisco' - stub_core.ssl_endpoint_list("example").returns([endpoint]) - - stdout, stderr = execute("certs:generate example.com") - - expect(request.domain).to eq("example.com") - expect(request.subject).to eq("/C=US/ST=California/L=San Francisco/O=Heroku/CN=example.com") - end - - it "not emitted if any part of subject is specified" do - expect_prompts() - stub_core.ssl_endpoint_list("example").returns([endpoint]) - - stdout, stderr = execute("certs:generate example.com --owner Heroku") - - expect(request.domain).to eq("example.com") - expect(request.subject).to eq("/O=Heroku/CN=example.com") - end - - it "not emitted if --now is specified" do - expect_prompts() - - stdout, stderr = execute("certs:generate example.com --now") - - expect(request.domain).to eq("example.com") - expect(request.subject).to eq("/CN=example.com") - end - - it "not emitted if --subject is specified" do - expect_prompts() - - stdout, stderr = execute("certs:generate example.com --subject SOMETHING") - - expect(request.domain).to eq("example.com") - expect(request.subject).to eq("SOMETHING") - end - - def expect_prompts(hash = {}) - hash.each do |question, answer| - expect_any_instance_of(Heroku::Command::Certs).to receive(:prompt).with(question).and_return(answer) - end - expect_any_instance_of(Heroku::Command::Certs).not_to receive(:prompt) - end - end - - describe "without --selfsigned" do - it "does not request a self-signed certificate" do - execute("certs:generate example.com --now") - expect(request.self_signed).to be false - end - - it "says it generated a key and CSR" do - stdout, stderr = execute("certs:generate example.com --now") - expect(stderr).to match(/^Your key and certificate signing request have been generated.$/) - end - - it "says the name of the CSR file" do - stdout, stderr = execute("certs:generate example.com --now") - expect(stderr).to match(/^Submit the CSR in 'csrfile' to your preferred certificate authority.$/) - end - end - - describe "with --selfsigned" do - it "requests a self-signed certificate" do - execute("certs:generate example.com --selfsigned --now") - expect(request.self_signed).to be true - end - - it "says it generated a key and self-signed certificate" do - stdout, stderr = execute("certs:generate example.com --selfsigned --now") - expect(stderr).to match(/^Your key and self-signed certificate have been generated.$/) - end - - it "says the name of the certificate file in the command" do - stdout, stderr = execute("certs:generate example.com --selfsigned --now") - expect(stderr).to match(/crtfile keyfile$/) - end - end - - describe "suggests next step" do - it "should be certs:add when domain is new" do - stdout, stderr = execute("certs:generate example.com --now") - expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) - end - - it "should be certs:update when domain is known" do - stdout, stderr = execute("certs:generate example.org --now") - expect(stderr).to match(/^\$ heroku certs:update CERTFILE keyfile$/) - end - - it "should be addons:add and certs:add when app doesn't have ssl:endpoint" do - stub_core.ssl_endpoint_list("example") { raise RestClient::Forbidden } - stdout, stderr = execute("certs:generate example.org --now") - expect(stderr).to match(/^\$ heroku addons:add ssl:endpoint$/) - expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) - end - end - - describe "key size" do - it "is 2048 unless otherwise specified" do - execute("certs:generate example.com --now") - expect(request.key_size).to eq(2048) - end - - it "can be changed using --keysize" do - execute("certs:generate example.com --now --keysize 4096") - expect(request.key_size).to eq(4096) - end - end - end - end end end diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb deleted file mode 100644 index 46cd6cd24..000000000 --- a/spec/heroku/open_ssl_spec.rb +++ /dev/null @@ -1,204 +0,0 @@ -require "heroku/open_ssl" - -describe Heroku::OpenSSL do - describe :openssl do - it "returns 'openssl' when nothing else is set" do - expect(Heroku::OpenSSL.openssl).to eq("openssl") - end - - it "returns the environment's 'OPENSSL' variable when it's set" do - ENV['OPENSSL'] = '/usr/bin/openssl' - expect(Heroku::OpenSSL.openssl).to eq('/usr/bin/openssl') - ENV['OPENSSL'] = nil - end - - it "can be set with openssl=" do - Heroku::OpenSSL.openssl = '/usr/local/bin/openssl' - expect(Heroku::OpenSSL.openssl).to eq('/usr/local/bin/openssl') - Heroku::OpenSSL.openssl = nil - end - - it "runs openssl(1) when passed arguments" do - expect(Heroku::OpenSSL).to receive(:system).with("openssl", "version").and_return(true) - expect(Heroku::OpenSSL.openssl("version")).to be true - end - end - - describe :ensure_openssl_installed! do - it "calls openssl(1) to ensure it's available" do - expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) - Heroku::OpenSSL.ensure_openssl_installed! - end - - it "detects openssl(1) is available when it is available" do - expect { Heroku::OpenSSL.ensure_openssl_installed! }.not_to raise_error - end - - it "detects openssl(1) is absent when it isn't available" do - Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' - expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) - Heroku::OpenSSL.openssl = nil - end - - it "gives good installation advice on a Mac" do - Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' - expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| - allow(ex).to receive(:running_on_a_mac?).and_return(true) - allow(ex).to receive(:running_on_windows?).and_return(false) - expect(ex.installation_hint).to match(/brew install openssl/) - } - Heroku::OpenSSL.openssl = nil - end - - it "gives good installation advice on Windows" do - Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' - expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| - allow(ex).to receive(:running_on_a_mac?).and_return(false) - allow(ex).to receive(:running_on_windows?).and_return(true) - expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) - } - Heroku::OpenSSL.openssl = nil - end - - it "gives good installation advice on miscellaneous Unixen" do - Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' - expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| - allow(ex).to receive(:running_on_a_mac?).and_return(false) - allow(ex).to receive(:running_on_windows?).and_return(false) - expect(ex.installation_hint).to match(/'openssl' package/) - } - Heroku::OpenSSL.openssl = nil - end - end - - describe :CertificateRequest do - it "initializes with good defaults" do - request = Heroku::OpenSSL::CertificateRequest.new - expect(request).not_to be_nil - expect(request.key_size).to eq(2048) - expect(request.self_signed).to be false - end - - context "generating with self_signed off" do - before(:all) do - @prev_dir = Dir.getwd - @dir = Dir.mktmpdir - Dir.chdir @dir - - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - - # Would like to do this, but the current version of rspec doesn't support it - # expect { result = request.generate }.to output.to_stdout_from_any_process - @result = request.generate - end - - it "should create Result object" do - expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result - end - - it "should have a key filename" do - expect(@result.key_file).to eq('example.com.key') - end - - it "should have a CSR filename" do - expect(@result.csr_file).to eq('example.com.csr') - end - - it "should not have a certificate filename" do - expect(@result.crt_file).to be_nil - end - - it "should produce a PEM key file" do - expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) - end - - it "should produce a PEM certificate file" do - expect(File.read(@result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") - end - - after(:all) do - Dir.chdir @prev_dir - FileUtils.remove_entry_secure @dir - end - end - - context "generating with self_signed on" do - before(:all) do - @prev_dir = Dir.getwd - @dir = Dir.mktmpdir - Dir.chdir @dir - - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - request.self_signed = true - - # Would like to do this, but the current version of rspec doesn't support it - # expect { result = request.generate }.to output.to_stdout_from_any_process - @result = request.generate - end - - it "should create Result object" do - expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result - end - - it "should have a key filename" do - expect(@result.key_file).to eq('example.com.key') - end - - it "should not have a CSR filename" do - expect(@result.csr_file).to be_nil - end - - it "should have a certificate filename" do - expect(@result.crt_file).to eq('example.com.crt') - end - - it "should produce a PEM key file" do - expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) - end - - it "should produce a PEM certificate file" do - expect(File.read(@result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") - end - - after(:all) do - Dir.chdir @prev_dir - FileUtils.remove_entry_secure @dir - end - end - - it "raises installation error when openssl(1) isn't installed" do - Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' - - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - - expect { result = request.generate }.to raise_error(Heroku::OpenSSL::NotInstalledError) - end - end - - Heroku::OpenSSL.openssl = nil - end - - it "uses key_size to control the key's size" do - skip "Can't be tested without an rspec supporting to_stdout_from_any_process" unless RSpec::Matchers::BuiltIn::Output.method_defined? :to_stdout_from_any_process - - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - request.key_size = 4096 - - expect { result = request.generate }.to output(/Generating a 4096 bit RSA private key/).to_stdout_from_any_process - end - end - end - end -end From a12a363338782f0ecae440ce794ad27f64eca934 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Mon, 12 Jan 2015 01:17:57 -0800 Subject: [PATCH 238/952] Revert "Merge pull request #1351 from heroku/revert-1346-feature/certs-generate" This reverts commit f9b02578981136fa6d4c445d91c9adc828e1ff02, reversing changes made to 4769577a6166653617bb5b25c1f593313483c217. --- lib/heroku/command/certs.rb | 101 ++++++++++++++- lib/heroku/open_ssl.rb | 93 ++++++++++++++ spec/heroku/command/certs_spec.rb | 143 +++++++++++++++++++++ spec/heroku/open_ssl_spec.rb | 204 ++++++++++++++++++++++++++++++ 4 files changed, 539 insertions(+), 2 deletions(-) create mode 100644 lib/heroku/open_ssl.rb create mode 100644 spec/heroku/open_ssl_spec.rb diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 894091e06..412e9e116 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -1,4 +1,5 @@ require "heroku/command/base" +require "heroku/open_ssl" require "excon" # manage ssl endpoints for an app @@ -152,7 +153,41 @@ def rollback display "New active certificate details:" display_certificate_info(endpoint) end - + + # certs:generate DOMAIN + # + # Generate a key and certificate signing request (or self-signed certificate) + # for an app. Prompts for information to put in the certificate unless --now + # is used, or at least one of the --subject, --owner, --country, --area, or + # --city options is specified. + # + # --selfsigned # generate a self-signed certificate instead of a CSR + # --keysize BITSIZE # RSA key size in bits (default: 2048) + # --owner NAME # name of organization certificate belongs to + # --country COUNTRY # country of owner, as a two-letter ISO country code + # --area AREA # sub-country area (state, province, etc.) of owner + # --city CITY # city of owner + # --subject SUBJECT # specify entire certificate subject + # --now # do not prompt for any owner information + def generate + request = Heroku::OpenSSL::CertificateRequest.new + + request.domain = args[0] || error("certs:generate must specify a domain") + request.subject = cert_subject_for_domain_and_options(request.domain, options) + request.self_signed = options[:selfsigned] || false + request.key_size = (options[:keysize] || request.key_size).to_i + + result = request.generate + + explain_step_after_generate result + + rescue Heroku::OpenSSL::NotInstalledError => ex + error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) + + rescue Heroku::OpenSSL::GenericError => ex + error(ex.message) + end + private def current_endpoint @@ -229,5 +264,67 @@ def read_crt_and_key_bypassing_ssl_doctor def read_crt_and_key options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor end - + + def all_endpoint_domains + endpoints = heroku.ssl_endpoint_list(app) + endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } \ + .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ + .reduce(:+) + end + + def prompt(question) + display("#{question}: ", false) + ask + end + + def val_empty?(val) + val.nil? or val.empty? + end + + def cert_subject_for_domain_and_options(domain, options = {}) + raise ArgumentError, "domain cannot be empty" if domain.nil? || domain.empty? + + subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now) + + if val_empty? subject + if !now && [country, area, city, owner].all? { |v| val_empty? v } + owner = prompt "Owner of this certificate" + country = prompt "Country of owner (two-letter ISO code)" + area = prompt "State/province/etc. of owner" + city = prompt "City of owner" + end + + subject = "" + subject += "/C=#{country}" unless val_empty? country + subject += "/ST=#{area}" unless val_empty? area + subject += "/L=#{city}" unless val_empty? city + subject += "/O=#{owner}" unless val_empty? owner + + subject += "/CN=#{domain}" + end + + subject + end + + def explain_step_after_generate(result) + if result.csr_file.nil? + display "Your key and self-signed certificate have been generated." + display "Next, run:" + else + display "Your key and certificate signing request have been generated." + display "Submit the CSR in '#{result.csr_file}' to your preferred certificate authority." + display "When you've received your certificate, run:" + end + + needs_addon = false + command = "add" + begin + command = "update" if all_endpoint_domains.include? result.request.domain + rescue RestClient::Forbidden + needs_addon = true + end + + display "$ heroku addons:add ssl:endpoint" if needs_addon + display "$ heroku certs:#{command} #{result.crt_file || "CERTFILE"} #{result.key_file}" + end end diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb new file mode 100644 index 000000000..e1941be6c --- /dev/null +++ b/lib/heroku/open_ssl.rb @@ -0,0 +1,93 @@ +require "heroku/helpers" +require "tempfile" + +module Heroku + module OpenSSL + def self.openssl(*args) + if args.empty? + ENV["OPENSSL"] || "openssl" + else + system(openssl, *args) + end + end + + def self.openssl=(val) + @checked = false + ENV["OPENSSL"] = val + end + + class CertificateRequest + attr_accessor :domain, :subject, :key_size, :self_signed + + def initialize() + @key_size = 2048 + @self_signed = false + super + end + + def generate + if self_signed + generate_self_signed_certificate + else + generate_csr + end + end + + class Result + attr_accessor :request, :key_file, :csr_file, :crt_file + + def initialize(request, key_file, csr_file, crt_file) + @request = request.dup + @key_file, @csr_file, @crt_file = key_file, csr_file, crt_file + end + end + + private + def generate_csr + keyfile = "#{domain}.key" + csrfile = "#{domain}.csr" + + openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" + + return Result.new(self, keyfile, csrfile, nil) + end + + def generate_self_signed_certificate + keyfile = "#{domain}.key" + crtfile = "#{domain}.crt" + + openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" + + return Result.new(self, keyfile, nil, crtfile) + end + + def openssl_req_new(keyfile, outfile, *args) + Heroku::OpenSSL.ensure_openssl_installed! + Heroku::OpenSSL.openssl("req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) + end + end + + class GenericError < StandardError; end + + class NotInstalledError < GenericError + include Heroku::Helpers + + def installation_hint + if running_on_a_mac? + "With Homebrew installed, run the following command:\n$ brew install openssl" + elsif running_on_windows? + "Download and install OpenSSL from ." + else + # Probably some kind of Linux or other Unix. Who knows what package manager they're using? + "Make sure your package manager's 'openssl' package is installed." + end + end + end + + def self.ensure_openssl_installed! + return if @checked + openssl("version") or raise NotInstalledError + @checked = true + end + end +end diff --git a/spec/heroku/command/certs_spec.rb b/spec/heroku/command/certs_spec.rb index 0c26d50ff..a5c36fca8 100644 --- a/spec/heroku/command/certs_spec.rb +++ b/spec/heroku/command/certs_spec.rb @@ -239,5 +239,148 @@ module Heroku::Command STDERR end end + + describe "certs:generate" do + context "fails early" do + it "if domain not specified" do + stdout, stderr = execute("certs:generate") + expect(stdout).to eq(" ! certs:generate must specify a domain\n") + end + end + + context "successfully" do + let(:request) do + request = Heroku::OpenSSL::CertificateRequest.new + expect(Heroku::OpenSSL::CertificateRequest).to receive(:new).and_return(request) + + expect(request).to receive(:generate) do + if request.self_signed + Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', nil, 'crtfile') + else + Heroku::OpenSSL::CertificateRequest::Result.new(request, 'keyfile', 'csrfile', nil) + end + end + + request + end + + before(:each) do + stub_core.ssl_endpoint_list("example").returns([endpoint]) + request() + end + + describe "with subject prompts" do + it "emitted if no parts of subject provided" do + expect_prompts /Owner/ => "Heroku", /Country/ => 'US', /State/ => 'California', /City/ => 'San Francisco' + stub_core.ssl_endpoint_list("example").returns([endpoint]) + + stdout, stderr = execute("certs:generate example.com") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/C=US/ST=California/L=San Francisco/O=Heroku/CN=example.com") + end + + it "not emitted if any part of subject is specified" do + expect_prompts() + stub_core.ssl_endpoint_list("example").returns([endpoint]) + + stdout, stderr = execute("certs:generate example.com --owner Heroku") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/O=Heroku/CN=example.com") + end + + it "not emitted if --now is specified" do + expect_prompts() + + stdout, stderr = execute("certs:generate example.com --now") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("/CN=example.com") + end + + it "not emitted if --subject is specified" do + expect_prompts() + + stdout, stderr = execute("certs:generate example.com --subject SOMETHING") + + expect(request.domain).to eq("example.com") + expect(request.subject).to eq("SOMETHING") + end + + def expect_prompts(hash = {}) + hash.each do |question, answer| + expect_any_instance_of(Heroku::Command::Certs).to receive(:prompt).with(question).and_return(answer) + end + expect_any_instance_of(Heroku::Command::Certs).not_to receive(:prompt) + end + end + + describe "without --selfsigned" do + it "does not request a self-signed certificate" do + execute("certs:generate example.com --now") + expect(request.self_signed).to be false + end + + it "says it generated a key and CSR" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^Your key and certificate signing request have been generated.$/) + end + + it "says the name of the CSR file" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^Submit the CSR in 'csrfile' to your preferred certificate authority.$/) + end + end + + describe "with --selfsigned" do + it "requests a self-signed certificate" do + execute("certs:generate example.com --selfsigned --now") + expect(request.self_signed).to be true + end + + it "says it generated a key and self-signed certificate" do + stdout, stderr = execute("certs:generate example.com --selfsigned --now") + expect(stderr).to match(/^Your key and self-signed certificate have been generated.$/) + end + + it "says the name of the certificate file in the command" do + stdout, stderr = execute("certs:generate example.com --selfsigned --now") + expect(stderr).to match(/crtfile keyfile$/) + end + end + + describe "suggests next step" do + it "should be certs:add when domain is new" do + stdout, stderr = execute("certs:generate example.com --now") + expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) + end + + it "should be certs:update when domain is known" do + stdout, stderr = execute("certs:generate example.org --now") + expect(stderr).to match(/^\$ heroku certs:update CERTFILE keyfile$/) + end + + it "should be addons:add and certs:add when app doesn't have ssl:endpoint" do + stub_core.ssl_endpoint_list("example") { raise RestClient::Forbidden } + stdout, stderr = execute("certs:generate example.org --now") + expect(stderr).to match(/^\$ heroku addons:add ssl:endpoint$/) + expect(stderr).to match(/^\$ heroku certs:add CERTFILE keyfile$/) + end + end + + describe "key size" do + it "is 2048 unless otherwise specified" do + execute("certs:generate example.com --now") + expect(request.key_size).to eq(2048) + end + + it "can be changed using --keysize" do + execute("certs:generate example.com --now --keysize 4096") + expect(request.key_size).to eq(4096) + end + end + end + end end end diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb new file mode 100644 index 000000000..46cd6cd24 --- /dev/null +++ b/spec/heroku/open_ssl_spec.rb @@ -0,0 +1,204 @@ +require "heroku/open_ssl" + +describe Heroku::OpenSSL do + describe :openssl do + it "returns 'openssl' when nothing else is set" do + expect(Heroku::OpenSSL.openssl).to eq("openssl") + end + + it "returns the environment's 'OPENSSL' variable when it's set" do + ENV['OPENSSL'] = '/usr/bin/openssl' + expect(Heroku::OpenSSL.openssl).to eq('/usr/bin/openssl') + ENV['OPENSSL'] = nil + end + + it "can be set with openssl=" do + Heroku::OpenSSL.openssl = '/usr/local/bin/openssl' + expect(Heroku::OpenSSL.openssl).to eq('/usr/local/bin/openssl') + Heroku::OpenSSL.openssl = nil + end + + it "runs openssl(1) when passed arguments" do + expect(Heroku::OpenSSL).to receive(:system).with("openssl", "version").and_return(true) + expect(Heroku::OpenSSL.openssl("version")).to be true + end + end + + describe :ensure_openssl_installed! do + it "calls openssl(1) to ensure it's available" do + expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) + Heroku::OpenSSL.ensure_openssl_installed! + end + + it "detects openssl(1) is available when it is available" do + expect { Heroku::OpenSSL.ensure_openssl_installed! }.not_to raise_error + end + + it "detects openssl(1) is absent when it isn't available" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) + Heroku::OpenSSL.openssl = nil + end + + it "gives good installation advice on a Mac" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + allow(ex).to receive(:running_on_a_mac?).and_return(true) + allow(ex).to receive(:running_on_windows?).and_return(false) + expect(ex.installation_hint).to match(/brew install openssl/) + } + Heroku::OpenSSL.openssl = nil + end + + it "gives good installation advice on Windows" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + allow(ex).to receive(:running_on_a_mac?).and_return(false) + allow(ex).to receive(:running_on_windows?).and_return(true) + expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) + } + Heroku::OpenSSL.openssl = nil + end + + it "gives good installation advice on miscellaneous Unixen" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| + allow(ex).to receive(:running_on_a_mac?).and_return(false) + allow(ex).to receive(:running_on_windows?).and_return(false) + expect(ex.installation_hint).to match(/'openssl' package/) + } + Heroku::OpenSSL.openssl = nil + end + end + + describe :CertificateRequest do + it "initializes with good defaults" do + request = Heroku::OpenSSL::CertificateRequest.new + expect(request).not_to be_nil + expect(request.key_size).to eq(2048) + expect(request.self_signed).to be false + end + + context "generating with self_signed off" do + before(:all) do + @prev_dir = Dir.getwd + @dir = Dir.mktmpdir + Dir.chdir @dir + + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + @result = request.generate + end + + it "should create Result object" do + expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result + end + + it "should have a key filename" do + expect(@result.key_file).to eq('example.com.key') + end + + it "should have a CSR filename" do + expect(@result.csr_file).to eq('example.com.csr') + end + + it "should not have a certificate filename" do + expect(@result.crt_file).to be_nil + end + + it "should produce a PEM key file" do + expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) + end + + it "should produce a PEM certificate file" do + expect(File.read(@result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") + end + + after(:all) do + Dir.chdir @prev_dir + FileUtils.remove_entry_secure @dir + end + end + + context "generating with self_signed on" do + before(:all) do + @prev_dir = Dir.getwd + @dir = Dir.mktmpdir + Dir.chdir @dir + + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + request.self_signed = true + + # Would like to do this, but the current version of rspec doesn't support it + # expect { result = request.generate }.to output.to_stdout_from_any_process + @result = request.generate + end + + it "should create Result object" do + expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result + end + + it "should have a key filename" do + expect(@result.key_file).to eq('example.com.key') + end + + it "should not have a CSR filename" do + expect(@result.csr_file).to be_nil + end + + it "should have a certificate filename" do + expect(@result.crt_file).to eq('example.com.crt') + end + + it "should produce a PEM key file" do + expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) + end + + it "should produce a PEM certificate file" do + expect(File.read(@result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") + end + + after(:all) do + Dir.chdir @prev_dir + FileUtils.remove_entry_secure @dir + end + end + + it "raises installation error when openssl(1) isn't installed" do + Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + + expect { result = request.generate }.to raise_error(Heroku::OpenSSL::NotInstalledError) + end + end + + Heroku::OpenSSL.openssl = nil + end + + it "uses key_size to control the key's size" do + skip "Can't be tested without an rspec supporting to_stdout_from_any_process" unless RSpec::Matchers::BuiltIn::Output.method_defined? :to_stdout_from_any_process + + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + request = Heroku::OpenSSL::CertificateRequest.new + request.domain = 'example.com' + request.subject = '/CN=example.com' + request.key_size = 4096 + + expect { result = request.generate }.to output(/Generating a 4096 bit RSA private key/).to_stdout_from_any_process + end + end + end + end +end From b9a758c692d985b66a87b5df5b5375a63d2c372c Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Mon, 12 Jan 2015 01:18:58 -0800 Subject: [PATCH 239/952] Fix ensure_openssl_installed! test flakiness ensure_openssl_installed! includes a flag indicating whether the current openssl(1) command has already been checked; this can make some tests fail if they are run in the wrong order. This patch ensures that said flag is always reset between ensure_openssl_installed! tests. --- spec/heroku/open_ssl_spec.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 46cd6cd24..9d08fecea 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -25,6 +25,12 @@ end describe :ensure_openssl_installed! do + after(:each) do + # This undoes any temporary changes to the property, and also + # resets the flag indicating the path has already been checked. + Heroku::OpenSSL.openssl = nil + end + it "calls openssl(1) to ensure it's available" do expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) Heroku::OpenSSL.ensure_openssl_installed! @@ -37,7 +43,6 @@ it "detects openssl(1) is absent when it isn't available" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) - Heroku::OpenSSL.openssl = nil end it "gives good installation advice on a Mac" do @@ -47,7 +52,6 @@ allow(ex).to receive(:running_on_windows?).and_return(false) expect(ex.installation_hint).to match(/brew install openssl/) } - Heroku::OpenSSL.openssl = nil end it "gives good installation advice on Windows" do @@ -57,7 +61,6 @@ allow(ex).to receive(:running_on_windows?).and_return(true) expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) } - Heroku::OpenSSL.openssl = nil end it "gives good installation advice on miscellaneous Unixen" do @@ -67,7 +70,6 @@ allow(ex).to receive(:running_on_windows?).and_return(false) expect(ex.installation_hint).to match(/'openssl' package/) } - Heroku::OpenSSL.openssl = nil end end From 6e316ffe8c5111d51e90f745767a61fa8b7d947c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 10:41:09 -0800 Subject: [PATCH 240/952] added description for rake pkg:release --- tasks/pkg.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks/pkg.rake b/tasks/pkg.rake index 5b6e4a4d2..ff84a6edd 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -55,6 +55,7 @@ end desc "build pkg" task "pkg:build" => dist("heroku-toolbelt-#{version}.pkg") +desc "release pkg" task "pkg:release" => dist("heroku-toolbelt-#{version}.pkg") do s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-#{version}.pkg" s3_store dist("heroku-toolbelt-#{version}.pkg"), "heroku-toolbelt/heroku-toolbelt-beta.pkg" if beta? From cb7b9fb1e267a36fee0a3ad761dde10b4c4ad16a Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 13 Jan 2015 10:52:52 -0800 Subject: [PATCH 241/952] updated release document --- RELEASE.md | 109 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index a98ec01fb..e6493e03f 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,38 +1,107 @@ Heroku CLI Release Process ========================== -### Ensure tests are passing +Releasing the CLI involves releasing a few different things. The important tasks can all be done on the buildserver. -* `bundle exec rake spec` - -### Prepare new version +### Releasing with buildserver +* Run test suite: `bundle exec rake` * Update version number in `lib/heroku/version.rb` to `X.Y.Z` * Bump the patch level `Z` if the release contains bugfixes that do not change functionality * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality -* Run `bundle install` to update the version of heroku in the Gemfile.lock +* Run `bundle install` to update the version of heroku in the `Gemfile.lock` * Update `CHANGELOG` +* Update Heroku Changelog (instructions below) * Commit the changes `git commit -m "vX.Y.Z" -a` * Push changes to master `git push origin master` +* Go to the buildserver and release http://54.148.200.17/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) +* [optional] Release the OSX pkg (instructions below) +* [optional] Release the WIN pkg (instructions below) + +### Notes + +The last 2 are optional because existing toolbelts will autoupdate after the first command is run. This isn't the case for deb packages which is why they're included in the main process. There can still be situations (although minor ones) where not releasing the osx/win packages can cause problems so they normally should always be run. + +The release process will prevent you from releasing an already released version. If you have a bad/incomplete release, you may need to bump the version number again. + +### Main Release + +This process releases the tgz (standalone/homebrew), zip (for autoupdates), deb package and ruby gem. It's everything that is required to not end up with a partial release. This is what the buildserver does for you, so you shouldn't have to do this manually (this is just for reference). Because this builds a deb package, you must be on an Ubuntu box. + +Prerequisites: + +* Running from Ubuntu +* Make sure you have permissions to `heroku` gem through `gem` https://rubygems.org/gems/heroku. +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* deb private key +* Ubuntu prerequisites: + +```sh +echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections +sudo apt-get install -y build-essential libpq-dev libsqlite3-dev curl xvfb wine +``` + +If this is your first time, you should first build the packages: `bundle exec rake build` Then look inside `./dist` to test each of the packages. + +Once you are confident it works, release: `bundle exec rake release`. Note that release will automatically build if the packages are not there (there is no need to run `rake build`). + +Note that you can look inside the `Rakefile` to test out each part of the step on your machine before it is built. + +### OSX Release + +Prerequisites: + +* OSX +* Heroku Developer ID Installer Certificate in Keychain +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` + +To build for testing: `bundle exec rake pkg:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.pkg`. +To release: `bundle exec rake pkg:release`. + +### Windows Release + +This is run not from a Windows machine, but from a UNIX machine with Wine. + +Mac Prerequisites: + +* Heroku Developer ID Installer Certificate in Keychain +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* Install [XQuartz](http://xquartz.macosforge.org/) manually, or via the terminal (restart required): + +```sh +curl -O# http://xquartz-dl.macosforge.org/SL/XQuartz-2.7.6.dmg +hdiutil attach XQuartz-2.7.6.dmg -mountpoint /Volumes/xquartz +sudo installer -store -pkg /Volumes/xquartz/XQuartz.pkg -target / +hdiutil detach /Volumes/xquartz +rm XQuartz-2.7.6.dmg +``` + +* `/opt/X11/bin` should be in your `$PATH` so `Xvfb` can be started. +* Install wine: `brew install wine` +* The pvk file: + +The certificate and private key for code signing are in the repo in: + +> dist/resources/exe/heroku-codesign-cert* + +which is in the format mono signcode wants. + +The pvk file is encrypted. If you want the build not to prompt you for +its passphrase, you'll need to decrypt it. See the `exe:pvk-nocrypt` task. + +Bewake the openssl version on the Mac doesn't work with `exe:pvk-nocrypt`. +See comments on the source code for details and solution. -### Release the gem +If you wanna leave the key encrypted, you still have to link it before +building; run the `exe:pvk` task for that. -* Ask @ddollar for: - * Permissions to Rubygems.org - * Access to the `toolbelt` Heroku app - * `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` config var values (export values in terminal) - * Access and permissions to run builds on http://dx-jenkins.herokai.com/ -* Release the gem `bundle exec rake release` - * Enter password for `sudo` during release - * Confirm gem release at http://rubygems.org/gems/heroku/versions +You'll have to ask the right person for the passphrase to the key. -### Release the toolbelt +You then need to initialize a custom wine build environment. The `exe:init-wine` +task will do that for you. -* Move to a checkout of the toolbelt repo and make sure everything is up to date `git pull` - - If this is a new checkout, run `git submodule init` and `git submodule update` -* Move to the components/heroku directory, `git fetch` and `git reset --hard HASH` where HASH is commit hash of vX.Y.Z -* Move back to the root dir of the toolbelt repo, stage `git add .`, commit `git commit -m "bump heroku submodule to vX.Y.Z"`, and push `git push` submodule changes -* Start toolbelt-build build at http://dx-jenkins.herokai.com/ (this will be opened by rake release automatically) +To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. +To release: `bundle exec rake pkg:release`. ### Changelog (only if there is at least one major new feature) From 4ec1d3c757346235d2cbd3ba283ce5aa63e01f77 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 13 Jan 2015 10:55:06 -0800 Subject: [PATCH 242/952] Update RELEASE.md --- RELEASE.md | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index e6493e03f..de327008e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,7 +3,7 @@ Heroku CLI Release Process Releasing the CLI involves releasing a few different things. The important tasks can all be done on the buildserver. -### Releasing with buildserver +## Releasing with buildserver * Run test suite: `bundle exec rake` * Update version number in `lib/heroku/version.rb` to `X.Y.Z` @@ -18,13 +18,13 @@ Releasing the CLI involves releasing a few different things. The important tasks * [optional] Release the OSX pkg (instructions below) * [optional] Release the WIN pkg (instructions below) -### Notes +## Notes The last 2 are optional because existing toolbelts will autoupdate after the first command is run. This isn't the case for deb packages which is why they're included in the main process. There can still be situations (although minor ones) where not releasing the osx/win packages can cause problems so they normally should always be run. The release process will prevent you from releasing an already released version. If you have a bad/incomplete release, you may need to bump the version number again. -### Main Release +## Main Release This process releases the tgz (standalone/homebrew), zip (for autoupdates), deb package and ruby gem. It's everything that is required to not end up with a partial release. This is what the buildserver does for you, so you shouldn't have to do this manually (this is just for reference). Because this builds a deb package, you must be on an Ubuntu box. @@ -47,7 +47,7 @@ Once you are confident it works, release: `bundle exec rake release`. Note that Note that you can look inside the `Rakefile` to test out each part of the step on your machine before it is built. -### OSX Release +## OSX Release Prerequisites: @@ -58,7 +58,7 @@ Prerequisites: To build for testing: `bundle exec rake pkg:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.pkg`. To release: `bundle exec rake pkg:release`. -### Windows Release +## Windows Release This is run not from a Windows machine, but from a UNIX machine with Wine. @@ -103,7 +103,16 @@ task will do that for you. To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. To release: `bundle exec rake pkg:release`. -### Changelog (only if there is at least one major new feature) +## Ruby versions + +Toolbelt bundles Ruby using different sources according to the OS: + +- Windows: fetches [rubyinstaller.exe](http://rubyinstaller.org/) from S3. +- Mac: fetches ruby.pkg from S3. That file was extracted from +[RailsInstaller](http://railsinstaller.org/en). +- Linux: uses system debs for Ruby. + +## Changelog (only if there is at least one major new feature) * Create a [new changelog](http://devcenter.heroku.com/admin/changelog_items/new) * Set the title to `Heroku CLI vX.Y.Z released with #{highlights}` From 5a7cbf9eca35375c00c78178c36245eb29c9b602 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 11:06:16 -0800 Subject: [PATCH 243/952] ensured newline is added before additions to startup file --- resources/pkg/postinstall | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/pkg/postinstall b/resources/pkg/postinstall index c072c83f3..f9cb2a062 100755 --- a/resources/pkg/postinstall +++ b/resources/pkg/postinstall @@ -22,6 +22,7 @@ install_path() { (grep "Added by the Heroku" $HOME/$file >/dev/null) && break cat <>$HOME/$file + ### Added by the Heroku Toolbelt export PATH="/usr/local/heroku/bin:\$PATH" MESSAGE From ffd81dc64c25872c9588e99e6e9eca7f85d60067 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 12:20:23 -0800 Subject: [PATCH 244/952] use git heroku.remote for setting remote in heroku git:remote also renamed remote extraction method to reflect what it does --- lib/heroku/command/base.rb | 4 ++-- lib/heroku/command/git.rb | 4 +--- spec/heroku/command/git_spec.rb | 2 ++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index a3c508c60..faf11f28a 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -217,7 +217,7 @@ def extract_app_in_dir(dir) if remote = options[:remote] remotes[remote] - elsif remote = extract_app_from_git_config + elsif remote = extract_remote_from_git_config remotes[remote] else apps = remotes.values.uniq @@ -229,7 +229,7 @@ def extract_app_in_dir(dir) end end - def extract_app_from_git_config + def extract_remote_from_git_config remote = git("config heroku.remote") remote == "" ? nil : remote end diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 73a2308d9..19051ed06 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -4,8 +4,6 @@ # class Heroku::Command::Git < Heroku::Command::Base - DEFAULT_REMOTE_NAME = 'heroku' - # git:clone APP [DIRECTORY] # # clones a heroku app to your local machine at DIRECTORY (defaults to app name) @@ -62,6 +60,6 @@ def remote private def remote_name - options[:remote] || DEFAULT_REMOTE_NAME + options[:remote] || extract_remote_from_git_config || 'heroku' end end diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb index 9d337f7f5..cace6af4a 100644 --- a/spec/heroku/command/git_spec.rb +++ b/spec/heroku/command/git_spec.rb @@ -105,6 +105,7 @@ module Heroku::Command it "adds remote" do any_instance_of(Heroku::Command::Git) do |git| + stub(git).git('config heroku.remote') stub(git).git('remote').returns("origin") stub(git).git('remote add heroku https://git.heroku.com/example.git') end @@ -129,6 +130,7 @@ module Heroku::Command it "updates remote when it already exists" do any_instance_of(Heroku::Command::Git) do |git| + stub(git).git('config heroku.remote') stub(git).git('remote').returns("heroku") stub(git).git('remote set-url heroku https://git.heroku.com/example.git') end From 06b566448c73a85720124c459eba6c7b509b8431 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 12:28:42 -0800 Subject: [PATCH 245/952] validate arguments for git:remote Fixes #1355 --- lib/heroku/command/git.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index 19051ed06..ddd446338 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -49,6 +49,7 @@ def clone # Git remote heroku added # def remote + validate_arguments! app_info = api.get_app(app).body if git('remote').split("\n").include?(remote_name) update_git_remote(remote_name, git_url(app_info['name'])) From 1134822160a4044924921eb1fc135892e5bf0354 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 7 Jan 2015 15:36:18 -0800 Subject: [PATCH 246/952] use http for hk download since it fails otherwise on windows --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 1cd68755d..3745569c7 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -95,7 +95,7 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/heroku-cli/master/manifest.json").body) + @manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/heroku-cli/master/manifest.json").body) end def self.url From 492f2c593fdf8965761cfa8a2303dc7322e1966c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 11 Jan 2015 13:41:17 -0800 Subject: [PATCH 247/952] cache setup? in jsplugins --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 3745569c7..6299cd2f6 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -2,7 +2,7 @@ class Heroku::JSPlugin include Heroku::Helpers def self.setup? - File.exists? bin + @is_setup ||= File.exists? bin end def self.load! From 69dbaa617a53246dfa6f051f218a7bc1eaa5e7fe Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 11 Jan 2015 13:53:09 -0800 Subject: [PATCH 248/952] use Heroku::Commands::Plugins command to display plugins --- lib/heroku/command/version.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/heroku/command/version.rb b/lib/heroku/command/version.rb index 9115376e3..84a5800c6 100644 --- a/lib/heroku/command/version.rb +++ b/lib/heroku/command/version.rb @@ -20,10 +20,6 @@ def index display(Heroku.user_agent) display(Heroku::JSPlugin.version) if Heroku::JSPlugin.setup? - plugins = Heroku::Plugin.list - if plugins.length > 0 - styled_header("Installed Plugins") - styled_array(plugins) - end + Heroku::Command::Plugins.new.index end end From 6e3ad0eb1e1a6e7a9c16d74d2ce9b2fced37c641 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 11 Jan 2015 16:22:02 -0800 Subject: [PATCH 249/952] added help text --- lib/heroku/jsplugin.rb | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 6299cd2f6..6b78f6735 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -1,5 +1,5 @@ class Heroku::JSPlugin - include Heroku::Helpers + extend Heroku::Helpers def self.setup? @is_setup ||= File.exists? bin @@ -8,6 +8,12 @@ def self.setup? def self.load! return unless setup? this = self + topics.each do |topic| + Heroku::Command.register_namespace( + :name => topic['name'], + :description => " #{topic['description']}" + ) unless Heroku::Command.namespaces.include?(topic['name']) + end commands.each do |plugin| klass = Class.new do def initialize(args, opts) @@ -17,14 +23,16 @@ def initialize(args, opts) end klass.send(:define_method, :run) do ENV['HEROKU_APP'] = @opts[:app] - exec this.bin, "#{plugin[:topic]}:#{plugin[:command]}", *@args + exec this.bin, "#{plugin['topic']}:#{plugin['command']}", *@args end - Heroku::Command.register_namespace(:name => plugin[:topic]) Heroku::Command.register_command( - :command => "#{plugin[:topic]}:#{plugin[:command]}", - :namespace => plugin[:topic], + :command => "#{plugin['topic']}:#{plugin['command']}", + :namespace => plugin['topic'], :klass => klass, - :method => :run + :method => :run, + :banner => plugin['usage'], + :summary => plugin['description'], + :help => "\n#{plugin['help']}" ) end end @@ -37,12 +45,16 @@ def self.plugins end end + def self.topics + commands_info['topics'] + end + def self.commands - @commands ||= `#{bin} commands`.split.flat_map do |l| - l.scan(/(\w+):(\w+)/).collect do |topic, command| - { :topic => topic, :command => command } - end - end + commands_info['commands'] + end + + def self.commands_info + @commands_info ||= json_decode(`#{bin} commands --json`) end def self.install(name) @@ -54,7 +66,7 @@ def self.version end def self.bin - File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") + File.join(home_directory, ".heroku", "heroku-cli") end def self.setup From 6b9f88fcc93d1ed9e9e9b39d9e0a758ce86ca2a3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 14:17:32 -0800 Subject: [PATCH 250/952] fixed tests --- lib/heroku/jsplugin.rb | 2 +- spec/heroku/command/version_spec.rb | 1 + spec/heroku/command_spec.rb | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 6b78f6735..0295e7cd4 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -66,7 +66,7 @@ def self.version end def self.bin - File.join(home_directory, ".heroku", "heroku-cli") + File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") end def self.setup diff --git a/spec/heroku/command/version_spec.rb b/spec/heroku/command/version_spec.rb index eb60f1571..28ce58684 100644 --- a/spec/heroku/command/version_spec.rb +++ b/spec/heroku/command/version_spec.rb @@ -9,6 +9,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT #{Heroku.user_agent} +You have no installed plugins. STDOUT end diff --git a/spec/heroku/command_spec.rb b/spec/heroku/command_spec.rb index 8e6581566..317af7ecc 100644 --- a/spec/heroku/command_spec.rb +++ b/spec/heroku/command_spec.rb @@ -171,6 +171,7 @@ class Heroku::Command::Test::Multiple; end it "displays the version if --version is used" do expect(heroku("--version")).to eq <<-STDOUT #{Heroku.user_agent} +You have no installed plugins. STDOUT end From 1a3d6497fb51dc8d6cd1c26691c7b9fd4ebb0259 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 14:24:57 -0800 Subject: [PATCH 251/952] cleaned up test output --- lib/heroku/helpers.rb | 8 ++++++++ lib/heroku/updater.rb | 5 +++-- spec/heroku/updater_spec.rb | 5 +++++ spec/spec_helper.rb | 1 + 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 8047b5623..e8f23323d 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -37,6 +37,14 @@ def debug(*args) $stderr.puts(*args) if debugging? end + def stderr_puts(*args) + $stderr.puts(*args) + end + + def stderr_print(*args) + $stderr.print(*args) + end + def debugging? ENV['HEROKU_DEBUG'] end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 2398e0c5a..a9334dcf3 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -4,6 +4,7 @@ module Heroku module Updater + extend Heroku::Helpers def self.error(message) raise Heroku::Command::CommandFailed.new(message) @@ -98,7 +99,7 @@ def self.autoupdate def self.update(prerelease=false) return unless prerelease || needs_update? - $stderr.print 'updating...' + stderr_print 'updating...' wait_for_lock do require "tmpdir" require "zip/zip" @@ -129,7 +130,7 @@ def self.update(prerelease=false) FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path - $stderr.puts "done. Updated to #{version}" + stderr_puts "done. Updated to #{version}" version end end diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index 8599664f1..cd9ecec51 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -5,6 +5,11 @@ module Heroku describe Updater do + before do + allow(subject).to receive(:stderr_puts) + allow(subject).to receive(:stderr_print) + end + describe('::latest_local_version') do it 'calculates the latest local version' do expect(subject.latest_local_version).to eq(Heroku::VERSION) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 505b4f7fb..483aaf806 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -131,6 +131,7 @@ def stub_core stub(Heroku::Auth).user.returns("email@example.com") stub(Heroku::Auth).password.returns("pass") stub(Heroku::Client).auth.returns("apikey01") + stub(Heroku::Updater).autoupdate stubbed_core end end From eb38bcfc42f034c44a5cadef201e4d75d888e609 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 14:31:04 -0800 Subject: [PATCH 252/952] v3.23.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 962d5cf7b..644a527c6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.23.0 2015-01-13 +================= +Added help for jsplugins +Fixed remote setting in git:remote +Fixed bug with newlines in .bashrc on OSX + 3.22.1 2015-01-05 ================= Updated cacert.pem diff --git a/Gemfile.lock b/Gemfile.lock index c3f304cae..7064c6145 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.22.1) + heroku (3.23.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index a771f48c0..2fa0370e9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.22.1" + VERSION = "3.23.0" end From 4b59a97d0d17b3b25688cce072e1c5a78b2a506c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Jan 2015 14:44:16 -0800 Subject: [PATCH 253/952] v3.23.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 644a527c6..dd0144f79 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.23.1 2015-01-13 +================= +Fixed authentication failure in release + 3.23.0 2015-01-13 ================= Added help for jsplugins diff --git a/Gemfile.lock b/Gemfile.lock index 7064c6145..a8f9ad672 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.23.0) + heroku (3.23.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2fa0370e9..87460107a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.23.0" + VERSION = "3.23.1" end From 52fc547f5c29a896e5c57a54cde8630158a05d92 Mon Sep 17 00:00:00 2001 From: Brent Royal-Gordon Date: Tue, 13 Jan 2015 22:26:57 -0800 Subject: [PATCH 254/952] Make openssl= resetting more robust Turned out there was still a case where "ensure_openssl_installed! calls openssl(1) to ensure it's available" would fail--if it was the first of the ensure_openssl_installed! tests to run, but ran after the CertificateRequest tests. This patch addresses this issue by resetting the openssl path much more aggressively: * Between every Heroku::OpenSSL test * Before the first Heroku::OpenSSL test This should completely isolate all tests in this file from any run of ensure_openssl_installed! during any other test. --- spec/heroku/open_ssl_spec.rb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 9d08fecea..71c3f7083 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -1,6 +1,15 @@ require "heroku/open_ssl" describe Heroku::OpenSSL do + # This undoes any temporary changes to the property, and also + # resets the flag indicating the path has already been checked. + before(:all) do + Heroku::OpenSSL.openssl = nil + end + after(:each) do + Heroku::OpenSSL.openssl = nil + end + describe :openssl do it "returns 'openssl' when nothing else is set" do expect(Heroku::OpenSSL.openssl).to eq("openssl") @@ -25,12 +34,6 @@ end describe :ensure_openssl_installed! do - after(:each) do - # This undoes any temporary changes to the property, and also - # resets the flag indicating the path has already been checked. - Heroku::OpenSSL.openssl = nil - end - it "calls openssl(1) to ensure it's available" do expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) Heroku::OpenSSL.ensure_openssl_installed! From b1abc99ce28b5f8036c1ecccb5de732f1a39797d Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Fri, 16 Jan 2015 09:35:19 -0800 Subject: [PATCH 255/952] Print Request-Id when HEROKU_DEBUG is on. --- lib/heroku/http_instrumentor.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb index 5dd364f4f..60e1d4fd6 100644 --- a/lib/heroku/http_instrumentor.rb +++ b/lib/heroku/http_instrumentor.rb @@ -17,6 +17,7 @@ def instrument(name, params={}, &block) $stderr.puts filter(params[:query]) when "excon.response" $stderr.puts "#{params[:status]} #{params[:reason_phrase]}" + $stderr.puts "request-id: #{headers['Request-id']}" if headers['Request-Id'] if headers['Content-Encoding'] == 'gzip' $stderr.puts filter(ungzip(params[:body])) else From 555a8be4ea965be1a42dbd563fe9e87d119c9184 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Jan 2015 11:31:01 -0800 Subject: [PATCH 256/952] just warn if plugins fail to load --- lib/heroku/jsplugin.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 0295e7cd4..b81c0c261 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -47,10 +47,16 @@ def self.plugins def self.topics commands_info['topics'] + rescue + $stderr.puts "error loading plugin topics" + return [] end def self.commands commands_info['commands'] + rescue + $stderr.puts "error loading plugin commands" + return [] end def self.commands_info From 30c488c0e0fc2da182851894c0d3b166d1d66b56 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Jan 2015 11:33:13 -0800 Subject: [PATCH 257/952] v3.23.2 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd0144f79..7bef855af 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.23.2 2015-01-16 +================= +Added certs:generate command +Fixed bug with plugins +Show request-id on HEROKU_DEBUG + 3.23.1 2015-01-13 ================= Fixed authentication failure in release diff --git a/Gemfile.lock b/Gemfile.lock index a8f9ad672..f71e65226 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.23.1) + heroku (3.23.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 87460107a..373505117 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.23.1" + VERSION = "3.23.2" end From 539f31bd6251261f202da1290c5fda7e0aa44901 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Jan 2015 11:43:15 -0800 Subject: [PATCH 258/952] dont autoupdate if it is disabled --- lib/heroku/updater.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index a9334dcf3..61420d5a2 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -87,6 +87,7 @@ def self.wait_for_lock(wait_for=5, check_every=0.5) end def self.autoupdate + return if disable # if we've updated in the last hour, don't try again if File.exists?(last_autoupdate_path) return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60 From 6d1e0ceab1e2ca8efb5b61e1f07037b331699dd6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Jan 2015 17:58:33 -0800 Subject: [PATCH 259/952] load jsplugins first so they can be overridden by core commands --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 04b63847a..75df9ab21 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -15,11 +15,11 @@ class << self end def self.load + Heroku::JSPlugin.load! Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file| require file end Heroku::Plugin.load! - Heroku::JSPlugin.load! unregister_commands_made_private_after_the_fact end From 2d1ada443d6dc5af2cf3b8d6e59fa3cdeb5c861d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Jan 2015 18:02:21 -0800 Subject: [PATCH 260/952] v3.23.3 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7bef855af..5321ee279 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.23.3 2015-01-16 +================= +Fixed bug where jsplugins could override core commands +Prevent non-autoupdatable clients from autoupdating + 3.23.2 2015-01-16 ================= Added certs:generate command diff --git a/Gemfile.lock b/Gemfile.lock index f71e65226..ca4ecf87a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.23.2) + heroku (3.23.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 373505117..b9c3975f7 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.23.2" + VERSION = "3.23.3" end From 83deb0f24cd66fbc3c3b654410746139796711d7 Mon Sep 17 00:00:00 2001 From: Naaman Newbold Date: Mon, 19 Jan 2015 17:52:02 -0800 Subject: [PATCH 261/952] Use Cedar Stack in `heroku create` Help This updates the `heroku create` help to show a cedar stack example instead of the [deprecated bamboo stack](https://devcenter.heroku.com/articles/bamboo). --- lib/heroku/command/apps.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 7ab1b4e3f..73668778c 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -206,9 +206,10 @@ def info # Creating floating-dragon-42... done, stack is cedar # http://floating-dragon-42.heroku.com/ | https://git.heroku.com/floating-dragon-42.git # - # $ heroku apps:create -s bamboo - # Creating floating-dragon-42... done, stack is bamboo-mri-1.9.2 - # http://floating-dragon-42.herokuapp.com/ | https://git.heroku.com/floating-dragon-42.git + # # specify a stack + # $ heroku create -s cedar + # Creating stormy-garden-5052... done, stack is cedar + # https://stormy-garden-5052.herokuapp.com/ | https://git.heroku.com/stormy-garden-5052.git # # # specify a name # $ heroku apps:create example From 9b7d149205f8f5d93b170896dd8013cd7564e72e Mon Sep 17 00:00:00 2001 From: Tim Mertens Date: Tue, 20 Jan 2015 10:44:37 -0600 Subject: [PATCH 262/952] Allow use of remote source if URI is specified as database. --- lib/heroku/command/pg.rb | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 63c4ac752..5545def0a 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -317,16 +317,13 @@ def push Heroku::Command.run(current_command, ['--help']) exit(1) end - if local =~ %r(://) - error "LOCAL_SOURCE_DATABASE is not a valid database name" - end - remote_uri = generate_resolver.resolve(remote).url - local_uri = "postgres:///#{local}" + target_uri = generate_resolver.resolve(remote).url + source_uri = parse_db_uri(local) pgdr = PgDumpRestore.new( - local_uri, - remote_uri, + source_uri, + target_uri, self) pgdr.execute @@ -343,16 +340,13 @@ def pull Heroku::Command.run(current_command, ['--help']) exit(1) end - if local =~ %r(://) - error "LOCAL_TARGET_DATABASE is not a valid database name" - end - remote_uri = generate_resolver.resolve(remote).url - local_uri = "postgres:///#{local}" + source_uri = generate_resolver.resolve(remote).url + target_uri = parse_db_uri(local) pgdr = PgDumpRestore.new( - remote_uri, - local_uri, + target_uri, + source_uri, self) pgdr.execute @@ -458,6 +452,14 @@ def generate_resolver Resolver.new(app_name, api) end + def parse_db_uri(local) + if local =~ %r(://) + return "postgres:///#{local}" + else + return local + end + end + def display_db(name, db) styled_header(name) From c1f5a2ff9c41424ea86a6088921da1230ba63b81 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 22 Jan 2015 14:36:46 -0800 Subject: [PATCH 263/952] use preauth for info command --- lib/heroku/command/apps.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 7ab1b4e3f..a74edaa30 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -95,6 +95,7 @@ def index # def info validate_arguments! + requires_preauth app_data = api.get_app(app).body unless options[:shell] From d2e9f7b55af3ac4a3308c4d7c727730dacf4f0b8 Mon Sep 17 00:00:00 2001 From: Tim Mertens Date: Mon, 26 Jan 2015 09:25:00 -0600 Subject: [PATCH 264/952] Fix db url parse logic. Better naming and documentation. Add tests. --- lib/heroku/command/pg.rb | 36 ++++++++++++++++++++++------------ spec/heroku/command/pg_spec.rb | 14 +++++++++++++ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 5545def0a..460beb27f 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -306,10 +306,14 @@ def killall end - # pg:push + # pg:push # - # push from LOCAL_SOURCE_DATABASE to REMOTE_TARGET_DATABASE + # push from SOURCE_DATABASE to REMOTE_TARGET_DATABASE # REMOTE_TARGET_DATABASE must be empty. + # + # SOURCE_DATABASE must be either the name of a database + # existing on your localhost or the fully qualified URL of + # a remote database. def push requires_preauth local, remote = shift_argument, shift_argument @@ -319,7 +323,7 @@ def push end target_uri = generate_resolver.resolve(remote).url - source_uri = parse_db_uri(local) + source_uri = parse_db_url(local) pgdr = PgDumpRestore.new( source_uri, @@ -329,10 +333,14 @@ def push pgdr.execute end - # pg:pull + # pg:pull + # + # pull from REMOTE_SOURCE_DATABASE to TARGET_DATABASE + # TARGET_DATABASE must not already exist. # - # pull from REMOTE_SOURCE_DATABASE to LOCAL_TARGET_DATABASE - # LOCAL_TARGET_DATABASE must not already exist. + # TARGET_DATABASE must be either the name of a database + # existing on your localhost or the fully qualified URL of + # a remote database. def pull requires_preauth remote, local = shift_argument, shift_argument @@ -342,7 +350,7 @@ def pull end source_uri = generate_resolver.resolve(remote).url - target_uri = parse_db_uri(local) + target_uri = parse_db_url(local) pgdr = PgDumpRestore.new( target_uri, @@ -452,12 +460,14 @@ def generate_resolver Resolver.new(app_name, api) end - def parse_db_uri(local) - if local =~ %r(://) - return "postgres:///#{local}" - else - return local - end + # Parse string database parameter and return string database URL. + # + # @param db_string [String] The local database name or a full connection URL, e.g. `my_db` or `postgres://user:pass@host:5432/my_db` + # @return [String] A full database connection URL. + def parse_db_url(db_string) + return db_string if db_string =~ %r(://) + + "postgres:///#{db_string}" end def display_db(name, db) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 1e437ec14..a68cf17c1 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -276,5 +276,19 @@ module Heroku::Command end end + describe '#parse_db_url' do + it 'returns a local url when only database name is supplied' do + pg = Heroku::Command::Pg.new + parsed_url = pg.send(:parse_db_url, 'MyLocalDb') + expect(parsed_url).to eql 'postgres:///MyLocalDb' + end + + it 'returns the original path when a url is specified' do + url = 'postgres://user:password@server:1234/'.freeze + pg = Heroku::Command::Pg.new + parsed_url = pg.send(:parse_db_url, url) + expect(parsed_url).to eql url + end + end end end From 646b29773548e2a383944f477aa7cf54bb9e87d5 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 26 Jan 2015 10:58:54 -0800 Subject: [PATCH 265/952] Move pg:backups commands from pg-extras into main CLI --- lib/heroku/client/heroku_postgresql.rb | 50 ++ .../client/heroku_postgresql_backups.rb | 115 +++++ lib/heroku/command/pg_backups.rb | 451 ++++++++++++++++++ 3 files changed, 616 insertions(+) create mode 100644 lib/heroku/client/heroku_postgresql_backups.rb create mode 100644 lib/heroku/command/pg_backups.rb diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index 4f1957be6..c812351e7 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -98,6 +98,48 @@ def maintenance_window_set(description) http_put "#{resource_name}/maintenance_window", 'description' => description end + # backups + def backups + http_get "#{resource_name}/transfers" + end + + def backups_get(id, verbose=false) + http_get "#{resource_name}/transfers/#{URI.encode(id)}?verbose=#{verbose}" + end + + def backups_capture + http_post "#{resource_name}/backups" + end + + def backups_restore(backup_url) + http_post "#{resource_name}/restores", 'backup_url' => backup_url + end + + def backups_delete(id) + http_delete "#{resource_name}/backups/#{URI.encode(id)}" + end + + def pg_copy(source_name, source_url, target_name, target_url) + http_post "#{resource_name}/transfers", { + 'from_name' => source_name, + 'from_url' => source_url, + 'to_name' => target_name, + 'to_url' => target_url, + } + end + + def schedules + http_get "#{resource_name}/transfer-schedules" + end + + def schedule(opts={}) + http_post "#{resource_name}/transfer-schedules", opts + end + + def unschedule(id) + http_delete "#{resource_name}/transfer-schedules/#{URI.encode(id.to_s)}" + end + protected def sym_keys(c) @@ -154,6 +196,14 @@ def http_put(path, payload = {}) end end + def http_delete(path) + checking_client_version do + response = heroku_postgresql_resource[path].delete + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + private def determine_host(value, default) diff --git a/lib/heroku/client/heroku_postgresql_backups.rb b/lib/heroku/client/heroku_postgresql_backups.rb new file mode 100644 index 000000000..237462911 --- /dev/null +++ b/lib/heroku/client/heroku_postgresql_backups.rb @@ -0,0 +1,115 @@ +class Heroku::Client::HerokuPostgresqlApp + + Version = 11 + + include Heroku::Helpers + + def self.headers + Heroku::Client::HerokuPostgresql.headers + end + + def initialize(app_name) + @app_name = app_name + end + + def transfers + http_get "#{@app_name}/transfers" + end + + def transfers_get(id, verbose=false) + http_get "#{@app_name}/transfers/#{URI.encode(id.to_s)}?verbose=#{verbose}" + end + + def transfers_delete(id) + http_delete "#{@app_name}/transfers/#{URI.encode(id.to_s)}" + end + + def transfers_cancel(id) + http_post "#{@app_name}/transfers/#{URI.encode(id.to_s)}/actions/cancel" + end + + def transfers_public_url(id) + http_post "#{@app_name}/transfers/#{URI.encode(id.to_s)}/actions/public-url" + end + + def heroku_postgresql_host + if ENV['SHOGUN'] + "shogun-#{ENV['SHOGUN']}.herokuapp.com" + else + determine_host(ENV["HEROKU_POSTGRESQL_HOST"], "postgres-api.heroku.com") + end + end + + def heroku_postgresql_resource + RestClient::Resource.new( + "https://#{heroku_postgresql_host}/client/v11/apps", + :user => Heroku::Auth.user, + :password => Heroku::Auth.password, + :headers => self.class.headers + ) + end + + def http_get(path) + checking_client_version do + retry_on_exception(RestClient::Exception) do + response = heroku_postgresql_resource[path].get + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + end + + def http_post(path, payload = {}) + checking_client_version do + response = heroku_postgresql_resource[path].post(json_encode(payload)) + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + + def http_delete(path) + checking_client_version do + response = heroku_postgresql_resource[path].delete + display_heroku_warning response + sym_keys(json_decode(response.to_s)) + end + end + + def display_heroku_warning(response) + warning = response.headers[:x_heroku_warning] + display warning if warning + response + end + + private + + def determine_host(value, default) + if value.nil? + default + else + "#{value}.herokuapp.com" + end + end + + def sym_keys(c) + if c.is_a?(Array) + c.map { |e| sym_keys(e) } + else + c.inject({}) do |h, (k, v)| + h[k.to_sym] = v; h + end + end + end + + def checking_client_version + begin + yield + rescue RestClient::BadRequest => e + if message = json_decode(e.response.to_s)["upgrade_message"] + abort(message) + else + raise e + end + end + end +end diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb new file mode 100644 index 000000000..6109f2875 --- /dev/null +++ b/lib/heroku/command/pg_backups.rb @@ -0,0 +1,451 @@ +require "heroku/client/heroku_postgresql" +require "heroku/client/heroku_postgresql_backups" +require "heroku/command/base" +require "heroku/helpers/heroku_postgresql" + +class Heroku::Command::Pg < Heroku::Command::Base + # pg:copy source target + # + # Copy all data from source database to target. At least one of + # these must be a Heroku Postgres database. + def copy + source_db = shift_argument + target_db = shift_argument + + validate_arguments! + + source = resolve_db_or_url(source_db) + target = resolve_db_or_url(target_db) + + if source.url == target.url + abort("Cannot copy database to itself") + end + + attachment = target.attachment || source.attachment + + xfer = hpg_client(attachment).pg_copy(source.name, source.url, + target.name, target.url) + poll_transfer('copy', xfer[:uuid]) + end + + # pg:backups [subcommand] + # + # Interact with built-in backups. Without a subcommand, it lists all + # available backups. The subcommands available are: + # + # info BACKUP_ID # get information about a specific backup + # capture DATABASE # capture a new backup + # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) + # public-url BACKUP_ID # get secret but publicly accessible URL for BACKUP_ID to download it + # cancel # cancel an in-progress backup + # delete BACKUP_ID # delete an existing backup + # schedule DATABASE # schedule nightly backups for given database + # --at ':00 ' # at a specific (24h clock) hour in the given timezone + # unschedule DATABASE # stop nightly backup for database + # schedules # list backup schedule + def backups + if args.count == 0 + list_backups + else + command = shift_argument + case command + when 'list' then list_backups + when 'info' then backup_status + when 'capture' then capture_backup + when 'restore' then restore_backup + when 'public-url' then public_url + when 'cancel' then cancel_backup + when 'delete' then delete_backup + when 'schedule' then schedule_backups + when 'unschedule' then unschedule_backups + when 'schedules' then list_schedules + else abort "Unknown pg:backups command: #{command}" + end + end + end + + private + + MaybeAttachment = Struct.new(:name, :url, :attachment) + + def url_name(uri) + "Database #{uri.path[1..-1]} on #{uri.host}:#{uri.port || 5432}" + end + + def resolve_db_or_url(name_or_url, default=nil) + if name_or_url =~ %r{postgres://} + url = name_or_url + uri = URI.parse(url) + name = url_name(uri) + MaybeAttachment.new(name, url, nil) + else + attachment = generate_resolver.resolve(name_or_url, default) + name = attachment.config_var.sub(/^HEROKU_POSTGRESQL_/, '').sub(/_URL$/, '') + MaybeAttachment.new(name, attachment.url, attachment) + end + end + + def arbitrary_app_db + generate_resolver.all_databases.values.first + end + + def transfer_name(backup_num, prefix='b') + "#{prefix}#{format("%03d", backup_num)}" + end + + def backup_num(transfer_name) + /b(\d+)/.match(transfer_name) && $1 + end + + def transfer_status(t) + if t[:finished_at] && t[:succeeded] + "Finished #{t[:finished_at]}" + elsif t[:finished_at] && !t[:succeeded] + "Failed #{t[:finished_at]}" + elsif t[:started_at] + "Running (processed #{size_pretty(t[:processed_bytes])})" + else + "Pending" + end + end + + def size_pretty(bytes) + suffixes = { + 'B' => 1, + 'kB' => 1_000, + 'MB' => 1_000_000, + 'GB' => 1_000_000_000, + 'TB' => 1_000_000_000_000 # (ohdear) + } + suffix, multiplier = suffixes.find do |k,v| + normalized = bytes / v.to_f + normalized >= 0 && normalized < 1_000 + end + if suffix.nil? + return bytes + end + normalized = bytes / multiplier.to_f + num_digits = case + when normalized >= 100 then '0' + when normalized >= 10 then '1' + else '2' + end + fmt_str = "%.#{num_digits}f#{suffix}" + format(fmt_str, normalized) + end + + def list_backups + validate_arguments! + transfers = hpg_app_client(app).transfers + + display "=== Backups" + display_backups = transfers.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end.sort_by { |b| b[:created_at] }.reverse.map do |b| + { + "id" => transfer_name(b[:num]), + "created_at" => b[:created_at], + "status" => transfer_status(b), + "size" => size_pretty(b[:processed_bytes]), + "database" => b[:from_name] || 'UNKNOWN' + } + end + if display_backups.empty? + error("No backups. Capture one with `heroku pg:backups capture`.") + else + display_table( + display_backups, + %w(id created_at status size database), + ["ID", "Backup Time", "Status", "Size", "Database"] + ) + end + + display "\n=== Restores" + display_restores = transfers.select do |r| + r[:from_type] == 'gof3r' && r[:to_type] == 'pg_restore' + end.sort_by { |r| r[:created_at] }.reverse.map do |r| + { + "id" => transfer_name(r[:num], 'r'), + "created_at" => r[:created_at], + "status" => transfer_status(r), + "size" => size_pretty(r[:processed_bytes]), + "database" => r[:from_name] || 'UNKNOWN' + } + end + if display_restores.empty? + error("No restores found. Use `heroku pg:backups restore` to restore a backup") + else + display_table( + display_restores, + %w(id created_at status size database), + ["ID", "Restore Time", "Status", "Size", "Database"] + ) + end + end + + def backup_status + backup_id = shift_argument + validate_arguments! + verbose = true + + client = hpg_app_client(app) + backup = if backup_id.nil? + backups = client.transfers + last_backup = backups.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end.sort_by { |b| b[:num] }.last + if last_backup.nil? + error("No backups. Capture one with `heroku pg:backups capture`.") + else + if verbose + client.transfers_get(last_backup[:num], verbose) + else + last_backup + end + end + else + client.transfers_get(backup_num(backup_id), verbose) + end + status = if backup[:succeeded] + "Completed Successfully" + elsif backup[:canceled_at] + "Canceled" + elsif backup[:finished_at] + "Failed" + elsif backup[:started_at] + "Running" + else + "Pending" + end + type = if backup[:schedule] + "Scheduled" + else + "Manual" + end + orig_size = backup[:source_bytes] + backup_size = backup[:processed_bytes] + compression_pct = [((orig_size - backup_size).to_f / orig_size * 100).round, 0].max + display <<-EOF +=== Backup info: #{backup_id} +Database: #{backup[:from_name]} +EOF + if backup[:started_at] + display <<-EOF +Started: #{backup[:started_at]} +EOF + end + if backup[:finished_at] + display <<-EOF +Finished: #{backup[:finished_at]} +EOF + end + display <<-EOF +Status: #{status} +Type: #{type} +EOF + if !orig_size.nil? && orig_size > 0 + display <<-EOF +Original DB Size: #{size_pretty(orig_size)} +Backup Size: #{size_pretty(backup_size)} (#{compression_pct}% compression) +EOF + else + display <<-EOF +Backup Size: #{size_pretty(backup_size)} +EOF + end + if verbose + display "=== Backup Logs" + backup[:logs].each do |item| + display "#{item['created_at']}: #{item['message']}" + end + end + end + + def capture_backup + db = shift_argument + attachment = generate_resolver.resolve(db, "DATABASE_URL") + validate_arguments! + + backup = hpg_client(attachment).backups_capture + display <<-EOF +Use Ctrl-C at any time to stop monitoring progress; the backup +will continue running. Use heroku pg:backups info to check progress. +Stop a running backup with heroku pg:backups cancel. + +#{attachment.name} ---backup---> #{transfer_name(backup[:num])} + +EOF + poll_transfer('backup', backup[:uuid]) + end + + def restore_backup + # heroku pg:backups restore [[backup_id] database] + db = nil + backup_id = :latest + + # N.B.: we have to account for the command argument here + if args.count == 2 + db = shift_argument + elsif args.count == 3 + backup_id = shift_argument + db = shift_argument + end + + attachment = generate_resolver.resolve(db, "DATABASE_URL") + validate_arguments! + + backups = hpg_app_client(app).transfers.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end + backup = if backup_id == :latest + # N.B.: this also handles the empty backups case + backups.sort_by { |b| b[:started_at] }.last + else + backups.find { |b| transfer_name(b[:num]) == backup_id } + end + if backups.empty? + abort("No backups. Capture one with `heroku pg:backups capture`.") + elsif backup.nil? + abort("Backup #{backup_id} not found.") + elsif !backup[:succeeded] + abort("Backup #{backup_id} did not complete successfully; cannot restore it.") + end + + backup = hpg_client(attachment).backups_restore(backup[:to_url]) + display <<-EOF +Use Ctrl-C at any time to stop monitoring progress; the backup +will continue restoring. Use heroku pg:backups to check progress. +Stop a running restore with heroku pg:backups cancel. + +#{transfer_name(backup[:num])} ---restore---> #{attachment.name} + +EOF + poll_transfer('restore', backup[:uuid]) + end + + def poll_transfer(action, transfer_id) + # pending, running, complete--poll endpoint to get + backup = nil + ticks = 0 + begin + backup = hpg_app_client(app).transfers_get(transfer_id) + status = if backup[:started_at] + "Running... #{size_pretty(backup[:processed_bytes])}" + else + "Pending... #{spinner(ticks)}" + end + redisplay status + ticks += 1 + sleep 1 + end until backup[:finished_at] + if backup[:succeeded] + redisplay "#{action.capitalize} completed\n" + else + # TODO: better errors for + # - db not online (/name or service not known/) + # - bad creds (/psql: FATAL:/???) + redisplay <<-EOF +An error occurred and your backup did not finish. + +Please run `heroku logs --ps pg-backups` for details. + +EOF + end + end + + def delete_backup + backup_id = shift_argument + validate_arguments! + + hpg_app_client(app).transfers_delete(backup_num(backup_id)) + display "Deleted #{backup_id}" + end + + def public_url + backup_id = shift_argument + validate_arguments! + + url_info = hpg_app_client(app).transfers_public_url(backup_num(backup_id)) + display "The following URL will expire at #{url_info[:expires_at]}:" + display " '#{url_info[:url]}'" + end + + def cancel_backup + validate_arguments! + + client = hpg_app_client(app) + transfer = client.transfers.find { |b| b[:finished_at].nil? } + client.transfers_cancel(transfer[:uuid]) + display "Canceled #{transfer_name(transfer[:num])}" + end + + def schedule_backups + db = shift_argument + validate_arguments! + at = options[:at] || '04:00 UTC' + schedule_opts = parse_schedule_time(at) + + attachment = generate_resolver.resolve(db, "DATABASE_URL") + hpg_client(attachment).schedule(schedule_opts) + display "Scheduled automatic daily backups at #{at} for #{attachment.name}" + end + + def unschedule_backups + db = shift_argument + validate_arguments! + + attachment = generate_resolver.resolve(db, "DATABASE_URL") + + schedule = hpg_client(attachment).schedules.find do |s| + attachment.name =~ /#{s[:name]}/ + end + + if schedule.nil? + display "No automatic daily backups for #{attachment.name} found" + else + hpg_client(attachment).unschedule(schedule[:uuid]) + display "Stopped automatic daily backups for #{attachment.name}" + end + end + + def list_schedules + validate_arguments! + attachment = arbitrary_app_db + + schedules = hpg_client(attachment).schedules + if schedules.empty? + display "No backup schedules found. Use `heroku pg:backups schedule` to set one up." + else + display "=== Backup Schedules" + schedules.each do |s| + display "#{s[:name]}: daily at #{s[:hour]}:00 (#{s[:timezone]})" + end + end + end + + def hpg_app_client(app_name) + Heroku::Client::HerokuPostgresqlApp.new(app_name) + end + + def parse_schedule_time(time_str) + hour, tz = time_str.match(/([0-2][0-9]):00 (.*)/) && [ $1, $2 ] + if hour.nil? || tz.nil? + abort("Invalid schedule format: expected ':00 '") + end + # do-what-i-mean remapping, since transferatu is (rightfully) picky + remap_tzs = { + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + 'MST' => 'America/Boise', + 'MDT' => 'America/Boise', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York' + } + if remap_tzs.has_key? tz.upcase + tz = remap_tzs[tz.upcase] + end + { hour: hour, timezone: tz } + end +end From 149b307dcb3765de40f96afd45de4f95f695a3e4 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 26 Jan 2015 14:04:47 -0800 Subject: [PATCH 266/952] Use 1.8.7-compatible hash syntax --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 6109f2875..50bfee4cc 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -446,6 +446,6 @@ def parse_schedule_time(time_str) if remap_tzs.has_key? tz.upcase tz = remap_tzs[tz.upcase] end - { hour: hour, timezone: tz } + { :hour => hour, :timezone => tz } end end From a523b8f32f419907486057abfc80595624f327d9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 26 Jan 2015 18:54:32 -0800 Subject: [PATCH 267/952] rollbar added for unhandled exceptions --- lib/heroku/cli.rb | 3 ++- lib/heroku/helpers.rb | 13 ++++--------- lib/heroku/rollbar.rb | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 lib/heroku/rollbar.rb diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index eab7030e0..58c1c1564 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -10,9 +10,10 @@ require 'heroku' require 'heroku/command' +require 'heroku/git' require 'heroku/helpers' require 'heroku/http_instrumentor' -require 'heroku/git' +require 'heroku/rollbar' require 'rest_client' require 'multi_json' require 'heroku-api' diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index e8f23323d..93291e13c 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -387,20 +387,13 @@ def styled_array(array, options={}) display end - def format_error(error, message='Heroku client internal error.') + def format_error(error, rollbar_id, message) formatted_error = [] formatted_error << " ! #{message}" formatted_error << ' ! Search for help at: https://help.heroku.com' formatted_error << ' ! Or report a bug at: https://github.com/heroku/heroku/issues/new' formatted_error << '' formatted_error << " Error: #{error.message} (#{error.class})" - formatted_error << " Backtrace: #{error.backtrace.first}" - error.backtrace[1..-1].each do |line| - formatted_error << " #{line}" - end - if error.backtrace.length > 1 - formatted_error << '' - end command = ARGV.map do |arg| if arg.include?(' ') arg = %{"#{arg}"} @@ -431,6 +424,7 @@ def format_error(error, message='Heroku client internal error.') end end formatted_error << " Version: #{Heroku.user_agent}" + formatted_error << " Error ID: #{rollbar_id}" if rollbar_id formatted_error << "\n" formatted_error.join("\n") end @@ -440,7 +434,8 @@ def styled_error(error, message='Heroku client internal error.') display("failed") Heroku::Helpers.error_with_failure = false end - $stderr.puts(format_error(error, message)) + rollbar_id = Rollbar.error(error) + $stderr.puts(format_error(error, rollbar_id, message)) end def styled_header(header) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb new file mode 100644 index 000000000..a838aa197 --- /dev/null +++ b/lib/heroku/rollbar.rb @@ -0,0 +1,43 @@ +module Rollbar + extend Heroku::Helpers + + def self.error(e) + payload = { + :access_token => 'f9ca108fdb4040479d539c7a649e2008', + :data => { + platform: 'client', + environment: 'production', + code_version: Heroku::VERSION, + client: { platform: RUBY_PLATFORM }, + request: { command: ARGV.join(' ') }, + body: { trace: trace_from_exception(e) } + } + } + p json_encode(payload) + response = Excon.post('https://api.rollbar.com/api/1/item/', body: json_encode(payload)) + p response.body + json_decode(response.body)["result"]["uuid"] + #rescue + #$stderr.puts "Error submitting error." + #nil + end + + private + + def self.trace_from_exception(e) + { + frames: frames_from_exception(e), + exception: { + class: e.class.to_s, + message: e.message + } + } + end + + def self.frames_from_exception(e) + e.backtrace.map do |line| + filename, lineno, method = line.scan(/(.+):(\d+):in `(.*)'/)[0] + { :filename => filename, :lineno => lineno.to_i, :method => method } + end + end +end From fe1823f6e79cc36922d540196ef8d96104d69f68 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 26 Jan 2015 18:57:03 -0800 Subject: [PATCH 268/952] removed rollbar debugging code --- lib/heroku/rollbar.rb | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index a838aa197..a71dea300 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -5,31 +5,29 @@ def self.error(e) payload = { :access_token => 'f9ca108fdb4040479d539c7a649e2008', :data => { - platform: 'client', - environment: 'production', - code_version: Heroku::VERSION, - client: { platform: RUBY_PLATFORM }, - request: { command: ARGV.join(' ') }, - body: { trace: trace_from_exception(e) } + :platform => 'client', + :environment => 'production', + :code_version => Heroku::VERSION, + :client => { :platform => RUBY_PLATFORM }, + :request => { :command => ARGV.join(' ') }, + :body => { :trace => trace_from_exception(e) } } } - p json_encode(payload) - response = Excon.post('https://api.rollbar.com/api/1/item/', body: json_encode(payload)) - p response.body + response = Excon.post('https://api.rollbar.com/api/1/item/', :body => json_encode(payload)) json_decode(response.body)["result"]["uuid"] - #rescue - #$stderr.puts "Error submitting error." - #nil + rescue + $stderr.puts "Error submitting error." + nil end private def self.trace_from_exception(e) { - frames: frames_from_exception(e), - exception: { - class: e.class.to_s, - message: e.message + :frames => frames_from_exception(e), + :exception => { + :class => e.class.to_s, + :message => e.message } } end From 2c1e1b516fbf7b00f060b00afa216b90680fa7bc Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 10:32:56 -0800 Subject: [PATCH 269/952] catch known errors like socket errors with rollbar --- lib/heroku/helpers.rb | 19 ++++++++++++++++-- lib/heroku/rollbar.rb | 46 +++++++++++++++++++++++++++++++++---------- spec/spec_helper.rb | 11 +++++++++++ 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 93291e13c..ce05651e9 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -280,6 +280,8 @@ def error(message) Heroku::Helpers.error_with_failure = false end $stderr.puts(format_with_bang(message)) + rollbar_id = Rollbar.error(message) + $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id exit(1) end @@ -387,7 +389,7 @@ def styled_array(array, options={}) display end - def format_error(error, rollbar_id, message) + def format_error(error, message='Heroku client internal error.', rollbar_id=nil) formatted_error = [] formatted_error << " ! #{message}" formatted_error << ' ! Search for help at: https://help.heroku.com' @@ -426,6 +428,8 @@ def format_error(error, rollbar_id, message) formatted_error << " Version: #{Heroku.user_agent}" formatted_error << " Error ID: #{rollbar_id}" if rollbar_id formatted_error << "\n" + formatted_error << " More information in #{error_log_path}" + formatted_error << "\n" formatted_error.join("\n") end @@ -435,7 +439,18 @@ def styled_error(error, message='Heroku client internal error.') Heroku::Helpers.error_with_failure = false end rollbar_id = Rollbar.error(error) - $stderr.puts(format_error(error, rollbar_id, message)) + $stderr.puts(format_error(error, message, rollbar_id)) + error_log(message, error.message, error.backtrace.join("\n")) + end + + def error_log(*obj) + File.open(error_log_path, 'a') do |file| + file.write(obj.join("\n") + "\n") + end + end + + def error_log_path + File.join(home_directory, '.heroku', 'error.log') end def styled_header(header) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index a71dea300..903210175 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -2,26 +2,52 @@ module Rollbar extend Heroku::Helpers def self.error(e) - payload = { + payload = json_encode(build_payload(e)) + response = Excon.post('https://api.rollbar.com/api/1/item/', :body => payload) + response = json_decode(response.body) + raise response if response["err"] != 0 + response["result"]["uuid"] + rescue => e + $stderr.puts "Error submitting error." + error_log(e.message, e.backtrace.join("\n")) + nil + end + + private + + def self.build_payload(e) + if e.is_a? Exception + build_trace_payload(e) + else + build_message_payload(e.to_s) + end + end + + def self.build_trace_payload(e) + payload = base_payload + payload[:data][:body] = {:trace => trace_from_exception(e)} + payload + end + + def self.build_message_payload(message) + payload = base_payload + payload[:data][:body] = {:message => {:body => message}} + payload + end + + def self.base_payload + { :access_token => 'f9ca108fdb4040479d539c7a649e2008', :data => { :platform => 'client', :environment => 'production', :code_version => Heroku::VERSION, :client => { :platform => RUBY_PLATFORM }, - :request => { :command => ARGV.join(' ') }, - :body => { :trace => trace_from_exception(e) } + :request => { :command => ARGV.join(' ') } } } - response = Excon.post('https://api.rollbar.com/api/1/item/', :body => json_encode(payload)) - json_decode(response.body)["result"]["uuid"] - rescue - $stderr.puts "Error submitting error." - nil end - private - def self.trace_from_exception(e) { :frames => frames_from_exception(e), diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 483aaf806..39f756f0b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -208,6 +208,12 @@ def home_directory def has_http_git_entry_in_netrc true end + undef_method :error_log + def error_log(*obj); end + undef_method :error_log_path + def error_log_path + 'error_log_path' + end end require "heroku/git" @@ -215,6 +221,11 @@ module Heroku::Git def self.check_git_version; end end +require "heroku/rollbar" +module Heroku::Rollbar + def self.error(e); end +end + require "support/display_message_matcher" require "support/organizations_mock_helper" From 7ec5a8221e10c9a891fb8b7120136ca482df5c1f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 11:55:37 -0800 Subject: [PATCH 270/952] v3.24.0 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5321ee279..5a805715a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.24.0 2015-01-27 +================= +Added error tracking with rollbar +Added pg:backups from pg-extras plugin +Allow db:push and db:pull to use remote databases (for setups like docker) +Fixed apps:info for paranoid apps +Upgraded excon to 0.43.0 + 3.23.3 2015-01-16 ================= Fixed bug where jsplugins could override core commands diff --git a/Gemfile.lock b/Gemfile.lock index ca4ecf87a..53c932bbf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.23.3) + heroku (3.24.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.42.1) + excon (0.43.0) fakefs (0.5.4) heroku-api (0.3.22) excon (~> 0.38) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index b9c3975f7..934ba92c1 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.23.3" + VERSION = "3.24.0" end From e08b3bbbe613a774c62c7100f07486d9e59f3853 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:22:39 -0800 Subject: [PATCH 271/952] skip reporting to rollbar on ctrl-c --- lib/heroku/cli.rb | 2 +- lib/heroku/helpers.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 58c1c1564..12631fac0 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -44,7 +44,7 @@ def self.start(*args) if ENV["HEROKU_DEBUG"] styled_error(e) else - error("Command cancelled.") + error("Command cancelled.", false) end rescue => error styled_error(error) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index ce05651e9..ba654d1d3 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -274,13 +274,13 @@ def output_with_bang(message="", new_line=true) display(format_with_bang(message), new_line) end - def error(message) + def error(message, report=true) if Heroku::Helpers.error_with_failure display("failed") Heroku::Helpers.error_with_failure = false end $stderr.puts(format_with_bang(message)) - rollbar_id = Rollbar.error(message) + rollbar_id = Rollbar.error(message) if report $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id exit(1) end From 08ef2c7082e76b991a9d856789c26c57e5b12958 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:24:30 -0800 Subject: [PATCH 272/952] hide command failed errors from rollbar --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 75df9ab21..53859fa06 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -269,7 +269,7 @@ def self.run(cmd, arguments=[]) error extract_error(e.http_body) end rescue CommandFailed => e - error e.message + error e.message, false rescue OptionParser::ParseError commands[cmd] ? run("help", [cmd]) : run("help") rescue Excon::Errors::SocketError, SocketError => e From f99c241cdd8b5236e23022d9b71bfc5d1df30704 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:26:37 -0800 Subject: [PATCH 273/952] v3.24.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 5a805715a..b6eec8f9b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.24.1 2015-01-27 +================= +Skip error reporting for errors like ctrl-c and command failures. + 3.24.0 2015-01-27 ================= Added error tracking with rollbar diff --git a/Gemfile.lock b/Gemfile.lock index 53c932bbf..aed73d432 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.24.0) + heroku (3.24.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 934ba92c1..f7294030f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.24.0" + VERSION = "3.24.1" end From 720a01efe2e952b8a255b3c4d88e83d70dc9daae Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:31:02 -0800 Subject: [PATCH 274/952] fixed addons spec --- spec/heroku/command/addons_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 590bf313f..83a2f6be7 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -307,7 +307,7 @@ module Heroku::Command end it "displays an error with unexpected options" do - expect(Heroku::Command).to receive(:error).with("Unexpected arguments: bar") + expect(Heroku::Command).to receive(:error).with("Unexpected arguments: bar", false) run("addons:add redistogo -a foo bar") end end From 34ea353907713f6076812c0baf9fdca68031b3da Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:39:27 -0800 Subject: [PATCH 275/952] disable rollbar since its too noisy --- lib/heroku/rollbar.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index 903210175..29892a38c 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -2,6 +2,8 @@ module Rollbar extend Heroku::Helpers def self.error(e) + # TODO: enable when ready + return payload = json_encode(build_payload(e)) response = Excon.post('https://api.rollbar.com/api/1/item/', :body => payload) response = json_decode(response.body) From cd01ae4eaf9ee3d488abdecfea4abd8a805f8dac Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:41:38 -0800 Subject: [PATCH 276/952] v3.24.2 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b6eec8f9b..f7a5e5b14 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.24.2 2015-01-27 +================= +Temporarily disable rollbar error reporting since it's too noisy + 3.24.1 2015-01-27 ================= Skip error reporting for errors like ctrl-c and command failures. diff --git a/Gemfile.lock b/Gemfile.lock index aed73d432..31b928457 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.24.1) + heroku (3.24.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f7294030f..08e105ab4 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.24.1" + VERSION = "3.24.2" end From 695677523fb44c9ede187deec5e7cbe46ffe28b7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:55:36 -0800 Subject: [PATCH 277/952] skip reporting of common errors by default. Only show first part of command for rollbar errors. --- lib/heroku/helpers.rb | 2 +- lib/heroku/rollbar.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index ba654d1d3..e1bedc759 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -274,7 +274,7 @@ def output_with_bang(message="", new_line=true) display(format_with_bang(message), new_line) end - def error(message, report=true) + def error(message, report=false) if Heroku::Helpers.error_with_failure display("failed") Heroku::Helpers.error_with_failure = false diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index 29892a38c..ba7502413 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -45,7 +45,7 @@ def self.base_payload :environment => 'production', :code_version => Heroku::VERSION, :client => { :platform => RUBY_PLATFORM }, - :request => { :command => ARGV.join(' ') } + :request => { :command => ARGV[0] } } } end From 57bf93a9a158fe4c4c43ba47a870d75691a27e3d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 12:58:08 -0800 Subject: [PATCH 278/952] re-enable rollbar --- lib/heroku/rollbar.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index ba7502413..3fb85da5f 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -2,8 +2,6 @@ module Rollbar extend Heroku::Helpers def self.error(e) - # TODO: enable when ready - return payload = json_encode(build_payload(e)) response = Excon.post('https://api.rollbar.com/api/1/item/', :body => payload) response = json_decode(response.body) From a99ee1d7a53a95914390c33cc28a0b711c6056f4 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 27 Jan 2015 15:24:40 -0800 Subject: [PATCH 279/952] Revert "Allow use of remote source if URI is specified as database." --- lib/heroku/command/pg.rb | 50 +++++++++++++--------------------- spec/heroku/command/pg_spec.rb | 14 ---------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 460beb27f..63c4ac752 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -306,14 +306,10 @@ def killall end - # pg:push + # pg:push # - # push from SOURCE_DATABASE to REMOTE_TARGET_DATABASE + # push from LOCAL_SOURCE_DATABASE to REMOTE_TARGET_DATABASE # REMOTE_TARGET_DATABASE must be empty. - # - # SOURCE_DATABASE must be either the name of a database - # existing on your localhost or the fully qualified URL of - # a remote database. def push requires_preauth local, remote = shift_argument, shift_argument @@ -321,26 +317,25 @@ def push Heroku::Command.run(current_command, ['--help']) exit(1) end + if local =~ %r(://) + error "LOCAL_SOURCE_DATABASE is not a valid database name" + end - target_uri = generate_resolver.resolve(remote).url - source_uri = parse_db_url(local) + remote_uri = generate_resolver.resolve(remote).url + local_uri = "postgres:///#{local}" pgdr = PgDumpRestore.new( - source_uri, - target_uri, + local_uri, + remote_uri, self) pgdr.execute end - # pg:pull + # pg:pull # - # pull from REMOTE_SOURCE_DATABASE to TARGET_DATABASE - # TARGET_DATABASE must not already exist. - # - # TARGET_DATABASE must be either the name of a database - # existing on your localhost or the fully qualified URL of - # a remote database. + # pull from REMOTE_SOURCE_DATABASE to LOCAL_TARGET_DATABASE + # LOCAL_TARGET_DATABASE must not already exist. def pull requires_preauth remote, local = shift_argument, shift_argument @@ -348,13 +343,16 @@ def pull Heroku::Command.run(current_command, ['--help']) exit(1) end + if local =~ %r(://) + error "LOCAL_TARGET_DATABASE is not a valid database name" + end - source_uri = generate_resolver.resolve(remote).url - target_uri = parse_db_url(local) + remote_uri = generate_resolver.resolve(remote).url + local_uri = "postgres:///#{local}" pgdr = PgDumpRestore.new( - target_uri, - source_uri, + remote_uri, + local_uri, self) pgdr.execute @@ -460,16 +458,6 @@ def generate_resolver Resolver.new(app_name, api) end - # Parse string database parameter and return string database URL. - # - # @param db_string [String] The local database name or a full connection URL, e.g. `my_db` or `postgres://user:pass@host:5432/my_db` - # @return [String] A full database connection URL. - def parse_db_url(db_string) - return db_string if db_string =~ %r(://) - - "postgres:///#{db_string}" - end - def display_db(name, db) styled_header(name) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index a68cf17c1..1e437ec14 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -276,19 +276,5 @@ module Heroku::Command end end - describe '#parse_db_url' do - it 'returns a local url when only database name is supplied' do - pg = Heroku::Command::Pg.new - parsed_url = pg.send(:parse_db_url, 'MyLocalDb') - expect(parsed_url).to eql 'postgres:///MyLocalDb' - end - - it 'returns the original path when a url is specified' do - url = 'postgres://user:password@server:1234/'.freeze - pg = Heroku::Command::Pg.new - parsed_url = pg.send(:parse_db_url, url) - expect(parsed_url).to eql url - end - end end end From 865ea8044515648dd5b1fa8de3dc1aeef9a78131 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 15:27:20 -0800 Subject: [PATCH 280/952] v3.24.3 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f7a5e5b14..689283b4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.24.3 2015-01-27 +================= +Reverted db:push and db:pull feature that allowed remote databases due to bugs +Enabled error tracking on unhandled exceptions + 3.24.2 2015-01-27 ================= Temporarily disable rollbar error reporting since it's too noisy diff --git a/Gemfile.lock b/Gemfile.lock index 31b928457..830135ac1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.24.2) + heroku (3.24.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 08e105ab4..e29e6cd94 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.24.2" + VERSION = "3.24.3" end From 9e0e79c3152418e61f6fc1d874881fae76e52495 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 15:42:11 -0800 Subject: [PATCH 281/952] rolled creds for rollbar --- lib/heroku/rollbar.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index 3fb85da5f..bda53e590 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -37,7 +37,7 @@ def self.build_message_payload(message) def self.base_payload { - :access_token => 'f9ca108fdb4040479d539c7a649e2008', + :access_token => '488f0c3af3d6450cb5b5827c8099dbff', :data => { :platform => 'client', :environment => 'production', From 8735ba56c02ff39a70eb626fa31595d2d089a128 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 15:42:54 -0800 Subject: [PATCH 282/952] v3.24.4 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 689283b4e..42b8f17bf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.24.4 2015-01-27 +================= +Rolled Rollbar creds + 3.24.3 2015-01-27 ================= Reverted db:push and db:pull feature that allowed remote databases due to bugs diff --git a/Gemfile.lock b/Gemfile.lock index 830135ac1..1847534d4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.24.3) + heroku (3.24.4) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e29e6cd94..966a8c774 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.24.3" + VERSION = "3.24.4" end From 72f8f8d2f53cc3131f005039431abf3296ce6b69 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 16:33:25 -0800 Subject: [PATCH 283/952] add disabling of rollbar via env var --- lib/heroku/rollbar.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index bda53e590..55977b7c4 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -2,6 +2,7 @@ module Rollbar extend Heroku::Helpers def self.error(e) + return if ENV['HEROKU_DISABLE_ERROR_REPORTING'] payload = json_encode(build_payload(e)) response = Excon.post('https://api.rollbar.com/api/1/item/', :body => payload) response = json_decode(response.body) From 6bd1bcf0f5ff090db26cd25a76397f0bd759d32a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 17:05:19 -0800 Subject: [PATCH 284/952] added ruby version to rollbar --- lib/heroku/rollbar.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index bda53e590..67d76bcde 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -42,7 +42,7 @@ def self.base_payload :platform => 'client', :environment => 'production', :code_version => Heroku::VERSION, - :client => { :platform => RUBY_PLATFORM }, + :client => { :platform => RUBY_PLATFORM, :ruby => RUBY_VERSION }, :request => { :command => ARGV[0] } } } From 391caa9fcb2f1af9ec0fccfb4916514c9e19c462 Mon Sep 17 00:00:00 2001 From: Tim Mertens Date: Tue, 27 Jan 2015 23:51:30 -0600 Subject: [PATCH 285/952] Revert "Revert "Allow use of remote source if URI is specified as database."" This reverts commit a99ee1d7a53a95914390c33cc28a0b711c6056f4. --- lib/heroku/command/pg.rb | 50 +++++++++++++++++++++------------- spec/heroku/command/pg_spec.rb | 14 ++++++++++ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 63c4ac752..460beb27f 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -306,10 +306,14 @@ def killall end - # pg:push + # pg:push # - # push from LOCAL_SOURCE_DATABASE to REMOTE_TARGET_DATABASE + # push from SOURCE_DATABASE to REMOTE_TARGET_DATABASE # REMOTE_TARGET_DATABASE must be empty. + # + # SOURCE_DATABASE must be either the name of a database + # existing on your localhost or the fully qualified URL of + # a remote database. def push requires_preauth local, remote = shift_argument, shift_argument @@ -317,25 +321,26 @@ def push Heroku::Command.run(current_command, ['--help']) exit(1) end - if local =~ %r(://) - error "LOCAL_SOURCE_DATABASE is not a valid database name" - end - remote_uri = generate_resolver.resolve(remote).url - local_uri = "postgres:///#{local}" + target_uri = generate_resolver.resolve(remote).url + source_uri = parse_db_url(local) pgdr = PgDumpRestore.new( - local_uri, - remote_uri, + source_uri, + target_uri, self) pgdr.execute end - # pg:pull + # pg:pull # - # pull from REMOTE_SOURCE_DATABASE to LOCAL_TARGET_DATABASE - # LOCAL_TARGET_DATABASE must not already exist. + # pull from REMOTE_SOURCE_DATABASE to TARGET_DATABASE + # TARGET_DATABASE must not already exist. + # + # TARGET_DATABASE must be either the name of a database + # existing on your localhost or the fully qualified URL of + # a remote database. def pull requires_preauth remote, local = shift_argument, shift_argument @@ -343,16 +348,13 @@ def pull Heroku::Command.run(current_command, ['--help']) exit(1) end - if local =~ %r(://) - error "LOCAL_TARGET_DATABASE is not a valid database name" - end - remote_uri = generate_resolver.resolve(remote).url - local_uri = "postgres:///#{local}" + source_uri = generate_resolver.resolve(remote).url + target_uri = parse_db_url(local) pgdr = PgDumpRestore.new( - remote_uri, - local_uri, + target_uri, + source_uri, self) pgdr.execute @@ -458,6 +460,16 @@ def generate_resolver Resolver.new(app_name, api) end + # Parse string database parameter and return string database URL. + # + # @param db_string [String] The local database name or a full connection URL, e.g. `my_db` or `postgres://user:pass@host:5432/my_db` + # @return [String] A full database connection URL. + def parse_db_url(db_string) + return db_string if db_string =~ %r(://) + + "postgres:///#{db_string}" + end + def display_db(name, db) styled_header(name) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 1e437ec14..a68cf17c1 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -276,5 +276,19 @@ module Heroku::Command end end + describe '#parse_db_url' do + it 'returns a local url when only database name is supplied' do + pg = Heroku::Command::Pg.new + parsed_url = pg.send(:parse_db_url, 'MyLocalDb') + expect(parsed_url).to eql 'postgres:///MyLocalDb' + end + + it 'returns the original path when a url is specified' do + url = 'postgres://user:password@server:1234/'.freeze + pg = Heroku::Command::Pg.new + parsed_url = pg.send(:parse_db_url, url) + expect(parsed_url).to eql url + end + end end end From 642fedf15ef96f62415ea972a02ea29983ba2d69 Mon Sep 17 00:00:00 2001 From: Tim Mertens Date: Wed, 28 Jan 2015 00:51:39 -0600 Subject: [PATCH 286/952] Fix pg pull bug. Add tests for pg pull & push --- lib/heroku/command/pg.rb | 10 ++++-- spec/heroku/command/pg_spec.rb | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 460beb27f..c206aa8a5 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -322,7 +322,7 @@ def push exit(1) end - target_uri = generate_resolver.resolve(remote).url + target_uri = resolve_heroku_url(remote) source_uri = parse_db_url(local) pgdr = PgDumpRestore.new( @@ -349,12 +349,12 @@ def pull exit(1) end - source_uri = generate_resolver.resolve(remote).url + source_uri = resolve_heroku_url(remote) target_uri = parse_db_url(local) pgdr = PgDumpRestore.new( - target_uri, source_uri, + target_uri, self) pgdr.execute @@ -455,6 +455,10 @@ def upgrade private + def resolve_heroku_url(remote) + generate_resolver.resolve(remote).url + end + def generate_resolver app_name = app rescue nil # will raise if no app, but calling app reads in arguments Resolver.new(app_name, api) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index a68cf17c1..43124154b 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -276,6 +276,66 @@ module Heroku::Command end end + describe '#push' do + context 'with remote and local dbs specified' do + let(:remote) { 'MY_HEROKU_DB_FUSCIA' } + let(:local) { 'MyLocalDb' } + + it 'executes dump restore with correct targets' do + pg = Heroku::Command::Pg.new + remote_url = "postgres://someurl.test/#{remote}" + local_url = "postgres:///#{local}" + dump_restore = double() + expect(pg).to receive(:resolve_heroku_url).and_return(remote_url) + expect(dump_restore).to receive(:execute) + expect(Heroku::Command).to receive(:shift_argument).and_return(local, remote) + expect(PgDumpRestore).to receive(:new).with(local_url, remote_url, pg).and_return(dump_restore) + + pg.push + end + end + + context 'with no databases specified' do + it 'displays help' do + pg = Heroku::Command::Pg.new + expect(pg).to receive(:current_command).and_return('push') + expect(Heroku::Command).to receive(:run).with('push', ['--help']) + + expect { pg.push }.to raise_error SystemExit + end + end + end + + describe '#pull' do + context 'with remote and local dbs specified' do + let(:remote) { 'MY_HEROKU_DB_FUSCIA' } + let(:local) { 'MyLocalDb' } + + it 'executes dump restore with correct targets' do + pg = Heroku::Command::Pg.new + remote_url = "postgres://someurl.test/#{remote}" + local_url = "postgres:///#{local}" + dump_restore = double() + expect(pg).to receive(:resolve_heroku_url).and_return(remote_url) + expect(dump_restore).to receive(:execute) + expect(Heroku::Command).to receive(:shift_argument).and_return(remote, local) + expect(PgDumpRestore).to receive(:new).with(remote_url, local_url, pg).and_return(dump_restore) + + pg.pull + end + + context 'with no databases specified' do + it 'displays help' do + pg = Heroku::Command::Pg.new + expect(pg).to receive(:current_command).and_return('pull') + expect(Heroku::Command).to receive(:run).with('pull', ['--help']) + + expect { pg.pull }.to raise_error SystemExit + end + end + end + end + describe '#parse_db_url' do it 'returns a local url when only database name is supplied' do pg = Heroku::Command::Pg.new From 5b74a0080e5c6c1641028de0970321dd08b2289a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 16:12:00 -0800 Subject: [PATCH 287/952] show better error with missing/misconfigured git --- lib/heroku/git.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/heroku/git.rb b/lib/heroku/git.rb index 19a4e44e0..aee49e6ab 100644 --- a/lib/heroku/git.rb +++ b/lib/heroku/git.rb @@ -38,10 +38,13 @@ def self.warn_about_insecure_git private def self.git_version - /git version ([\d\.]+)/.match(`git --version`)[1] + version = /git version ([\d\.]+)/.match(`git --version`) + error("Git appears to be installed incorrectly\nEnsure that `git --version` outputs the version correctly.") unless version + version[1] + rescue Errno::ENOENT + error("Git must be installed to use the Heroku Toolbelt.\nSee instructions here: http://git-scm.com") end - class Version include Comparable From 16bdf36ef8b00ab980b084298f159f64bcf3480f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 28 Jan 2015 15:38:39 -0800 Subject: [PATCH 288/952] fixed credential warning message to say warning not error Fixes #1387 --- lib/heroku/command/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index faf11f28a..c576fb628 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -265,7 +265,7 @@ def git_url(app_name) "git@#{Heroku::Auth.git_host}:#{app_name}.git" else unless has_http_git_entry_in_netrc - warn "ERROR: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" + warn "WARNING: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" exit 1 end "https://#{Heroku::Auth.http_git_host}/#{app_name}.git" From d2499f599db4edd16a8ac01539969e009b29be76 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 27 Jan 2015 16:05:10 -0800 Subject: [PATCH 289/952] handle broken pipes cleanly --- lib/heroku/cli.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 12631fac0..eb44b6a02 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -39,6 +39,8 @@ def self.start(*args) Heroku::Command.load Heroku::Command.run(command, args) Heroku::Updater.autoupdate + rescue Errno::EPIPE => e + error(e.message) rescue Interrupt => e `stty icanon echo` if ENV["HEROKU_DEBUG"] From 45b3ad7371d41cf0196c002c4ebe874704730d85 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 28 Jan 2015 15:54:47 -0800 Subject: [PATCH 290/952] v3.24.5 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 42b8f17bf..c52c47c89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.24.5 2015-01-28 +================= +Better errors when git is not found or not working +Fixed bug with unhandled EPIPE exception +Fixed credential warning message +Added ruby version to exception tracking +Added disabling of error tracking with HEROKU_DISABLE_ERROR_REPORTING + 3.24.4 2015-01-27 ================= Rolled Rollbar creds diff --git a/Gemfile.lock b/Gemfile.lock index 1847534d4..dd93a94ee 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.24.4) + heroku (3.24.5) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 966a8c774..d4bf0875a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.24.4" + VERSION = "3.24.5" end From 6ccaf942fbf82528aaa04c7ae26277b068f7f702 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 28 Jan 2015 17:26:50 -0800 Subject: [PATCH 291/952] added okjson for people that have not updated their plugins --- lib/vendor/heroku/okjson.rb | 598 ++++++++++++++++++++++++++++++++++++ 1 file changed, 598 insertions(+) create mode 100644 lib/vendor/heroku/okjson.rb diff --git a/lib/vendor/heroku/okjson.rb b/lib/vendor/heroku/okjson.rb new file mode 100644 index 000000000..72aa6c81b --- /dev/null +++ b/lib/vendor/heroku/okjson.rb @@ -0,0 +1,598 @@ +# encoding: UTF-8 +# +# Copyright 2011, 2012 Keith Rarick +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +# See https://github.com/kr/okjson for updates. + +require 'stringio' + +# Some parts adapted from +# http://golang.org/src/pkg/json/decode.go and +# http://golang.org/src/pkg/utf8/utf8.go +module Heroku + module OkJson + extend self + + + # Decodes a json document in string s and + # returns the corresponding ruby value. + # String s must be valid UTF-8. If you have + # a string in some other encoding, convert + # it first. + # + # String values in the resulting structure + # will be UTF-8. + def decode(s) + ts = lex(s) + v, ts = textparse(ts) + if ts.length > 0 + raise Error, 'trailing garbage' + end + v + end + + + # Parses a "json text" in the sense of RFC 4627. + # Returns the parsed value and any trailing tokens. + # Note: this is almost the same as valparse, + # except that it does not accept atomic values. + def textparse(ts) + if ts.length < 0 + raise Error, 'empty' + end + + typ, _, val = ts[0] + case typ + when '{' then objparse(ts) + when '[' then arrparse(ts) + else + raise Error, "unexpected #{val.inspect}" + end + end + + + # Parses a "value" in the sense of RFC 4627. + # Returns the parsed value and any trailing tokens. + def valparse(ts) + if ts.length < 0 + raise Error, 'empty' + end + + typ, _, val = ts[0] + case typ + when '{' then objparse(ts) + when '[' then arrparse(ts) + when :val,:str then [val, ts[1..-1]] + else + raise Error, "unexpected #{val.inspect}" + end + end + + + # Parses an "object" in the sense of RFC 4627. + # Returns the parsed value and any trailing tokens. + def objparse(ts) + ts = eat('{', ts) + obj = {} + + if ts[0][0] == '}' + return obj, ts[1..-1] + end + + k, v, ts = pairparse(ts) + obj[k] = v + + if ts[0][0] == '}' + return obj, ts[1..-1] + end + + loop do + ts = eat(',', ts) + + k, v, ts = pairparse(ts) + obj[k] = v + + if ts[0][0] == '}' + return obj, ts[1..-1] + end + end + end + + + # Parses a "member" in the sense of RFC 4627. + # Returns the parsed values and any trailing tokens. + def pairparse(ts) + (typ, _, k), ts = ts[0], ts[1..-1] + if typ != :str + raise Error, "unexpected #{k.inspect}" + end + ts = eat(':', ts) + v, ts = valparse(ts) + [k, v, ts] + end + + + # Parses an "array" in the sense of RFC 4627. + # Returns the parsed value and any trailing tokens. + def arrparse(ts) + ts = eat('[', ts) + arr = [] + + if ts[0][0] == ']' + return arr, ts[1..-1] + end + + v, ts = valparse(ts) + arr << v + + if ts[0][0] == ']' + return arr, ts[1..-1] + end + + loop do + ts = eat(',', ts) + + v, ts = valparse(ts) + arr << v + + if ts[0][0] == ']' + return arr, ts[1..-1] + end + end + end + + + def eat(typ, ts) + if ts[0][0] != typ + raise Error, "expected #{typ} (got #{ts[0].inspect})" + end + ts[1..-1] + end + + + # Scans s and returns a list of json tokens, + # excluding white space (as defined in RFC 4627). + def lex(s) + ts = [] + while s.length > 0 + typ, lexeme, val = tok(s) + if typ == nil + raise Error, "invalid character at #{s[0,10].inspect}" + end + if typ != :space + ts << [typ, lexeme, val] + end + s = s[lexeme.length..-1] + end + ts + end + + + # Scans the first token in s and + # returns a 3-element list, or nil + # if s does not begin with a valid token. + # + # The first list element is one of + # '{', '}', ':', ',', '[', ']', + # :val, :str, and :space. + # + # The second element is the lexeme. + # + # The third element is the value of the + # token for :val and :str, otherwise + # it is the lexeme. + def tok(s) + case s[0] + when ?{ then ['{', s[0,1], s[0,1]] + when ?} then ['}', s[0,1], s[0,1]] + when ?: then [':', s[0,1], s[0,1]] + when ?, then [',', s[0,1], s[0,1]] + when ?[ then ['[', s[0,1], s[0,1]] + when ?] then [']', s[0,1], s[0,1]] + when ?n then nulltok(s) + when ?t then truetok(s) + when ?f then falsetok(s) + when ?" then strtok(s) + when Spc then [:space, s[0,1], s[0,1]] + when ?\t then [:space, s[0,1], s[0,1]] + when ?\n then [:space, s[0,1], s[0,1]] + when ?\r then [:space, s[0,1], s[0,1]] + else numtok(s) + end + end + + + def nulltok(s); s[0,4] == 'null' ? [:val, 'null', nil] : [] end + def truetok(s); s[0,4] == 'true' ? [:val, 'true', true] : [] end + def falsetok(s); s[0,5] == 'false' ? [:val, 'false', false] : [] end + + + def numtok(s) + m = /-?([1-9][0-9]+|[0-9])([.][0-9]+)?([eE][+-]?[0-9]+)?/.match(s) + if m && m.begin(0) == 0 + if m[3] && !m[2] + [:val, m[0], Integer(m[1])*(10**Integer(m[3][1..-1]))] + elsif m[2] + [:val, m[0], Float(m[0])] + else + [:val, m[0], Integer(m[0])] + end + else + [] + end + end + + + def strtok(s) + m = /"([^"\\]|\\["\/\\bfnrt]|\\u[0-9a-fA-F]{4})*"/.match(s) + if ! m + raise Error, "invalid string literal at #{abbrev(s)}" + end + [:str, m[0], unquote(m[0])] + end + + + def abbrev(s) + t = s[0,10] + p = t['`'] + t = t[0,p] if p + t = t + '...' if t.length < s.length + '`' + t + '`' + end + + + # Converts a quoted json string literal q into a UTF-8-encoded string. + # The rules are different than for Ruby, so we cannot use eval. + # Unquote will raise an error if q contains control characters. + def unquote(q) + q = q[1...-1] + rubydoesenc = false + # In ruby >= 1.9, a[w] is a codepoint, not a byte. + if q.class.method_defined?(:force_encoding) + q.force_encoding('UTF-8') + rubydoesenc = true + end + a = q.dup # allocate a big enough string + r, w = 0, 0 + while r < q.length + c = q[r] + case true + when c == ?\\ + r += 1 + if r >= q.length + raise Error, "string literal ends with a \"\\\": \"#{q}\"" + end + + case q[r] + when ?",?\\,?/,?' + a[w] = q[r] + r += 1 + w += 1 + when ?b,?f,?n,?r,?t + a[w] = Unesc[q[r]] + r += 1 + w += 1 + when ?u + r += 1 + uchar = begin + hexdec4(q[r,4]) + rescue RuntimeError => e + raise Error, "invalid escape sequence \\u#{q[r,4]}: #{e}" + end + r += 4 + if surrogate? uchar + if q.length >= r+6 + uchar1 = hexdec4(q[r+2,4]) + uchar = subst(uchar, uchar1) + if uchar != Ucharerr + # A valid pair; consume. + r += 6 + end + end + end + if rubydoesenc + a[w] = '' << uchar + w += 1 + else + w += ucharenc(a, w, uchar) + end + else + raise Error, "invalid escape char #{q[r]} in \"#{q}\"" + end + when c == ?", c < Spc + raise Error, "invalid character in string literal \"#{q}\"" + else + # Copy anything else byte-for-byte. + # Valid UTF-8 will remain valid UTF-8. + # Invalid UTF-8 will remain invalid UTF-8. + # In ruby >= 1.9, c is a codepoint, not a byte, + # in which case this is still what we want. + a[w] = c + r += 1 + w += 1 + end + end + a[0,w] + end + + + # Encodes unicode character u as UTF-8 + # bytes in string a at position i. + # Returns the number of bytes written. + def ucharenc(a, i, u) + case true + when u <= Uchar1max + a[i] = (u & 0xff).chr + 1 + when u <= Uchar2max + a[i+0] = (Utag2 | ((u>>6)&0xff)).chr + a[i+1] = (Utagx | (u&Umaskx)).chr + 2 + when u <= Uchar3max + a[i+0] = (Utag3 | ((u>>12)&0xff)).chr + a[i+1] = (Utagx | ((u>>6)&Umaskx)).chr + a[i+2] = (Utagx | (u&Umaskx)).chr + 3 + else + a[i+0] = (Utag4 | ((u>>18)&0xff)).chr + a[i+1] = (Utagx | ((u>>12)&Umaskx)).chr + a[i+2] = (Utagx | ((u>>6)&Umaskx)).chr + a[i+3] = (Utagx | (u&Umaskx)).chr + 4 + end + end + + + def hexdec4(s) + if s.length != 4 + raise Error, 'short' + end + (nibble(s[0])<<12) | (nibble(s[1])<<8) | (nibble(s[2])<<4) | nibble(s[3]) + end + + + def subst(u1, u2) + if Usurr1 <= u1 && u1 < Usurr2 && Usurr2 <= u2 && u2 < Usurr3 + return ((u1-Usurr1)<<10) | (u2-Usurr2) + Usurrself + end + return Ucharerr + end + + + def surrogate?(u) + Usurr1 <= u && u < Usurr3 + end + + + def nibble(c) + case true + when ?0 <= c && c <= ?9 then c.ord - ?0.ord + when ?a <= c && c <= ?z then c.ord - ?a.ord + 10 + when ?A <= c && c <= ?Z then c.ord - ?A.ord + 10 + else + raise Error, "invalid hex code #{c}" + end + end + + + # Encodes x into a json text. It may contain only + # Array, Hash, String, Numeric, true, false, nil. + # (Note, this list excludes Symbol.) + # X itself must be an Array or a Hash. + # No other value can be encoded, and an error will + # be raised if x contains any other value, such as + # Nan, Infinity, Symbol, and Proc, or if a Hash key + # is not a String. + # Strings contained in x must be valid UTF-8. + def encode(x) + case x + when Hash then objenc(x) + when Array then arrenc(x) + else + raise Error, 'root value must be an Array or a Hash' + end + end + + + def valenc(x) + case x + when Hash then objenc(x) + when Array then arrenc(x) + when String then strenc(x) + when Numeric then numenc(x) + when true then "true" + when false then "false" + when nil then "null" + else + raise Error, "cannot encode #{x.class}: #{x.inspect}" + end + end + + + def objenc(x) + '{' + x.map{|k,v| keyenc(k) + ':' + valenc(v)}.join(',') + '}' + end + + + def arrenc(a) + '[' + a.map{|x| valenc(x)}.join(',') + ']' + end + + + def keyenc(k) + case k + when String then strenc(k) + else + raise Error, "Hash key is not a string: #{k.inspect}" + end + end + + + def strenc(s) + t = StringIO.new + t.putc(?") + r = 0 + + # In ruby >= 1.9, s[r] is a codepoint, not a byte. + rubydoesenc = s.class.method_defined?(:encoding) + + while r < s.length + case s[r] + when ?" then t.print('\\"') + when ?\\ then t.print('\\\\') + when ?\b then t.print('\\b') + when ?\f then t.print('\\f') + when ?\n then t.print('\\n') + when ?\r then t.print('\\r') + when ?\t then t.print('\\t') + else + c = s[r] + case true + when rubydoesenc + begin + c.ord # will raise an error if c is invalid UTF-8 + t.write(c) + rescue + t.write(Ustrerr) + end + when Spc <= c && c <= ?~ + t.putc(c) + else + n = ucharcopy(t, s, r) # ensure valid UTF-8 output + r += n - 1 # r is incremented below + end + end + r += 1 + end + t.putc(?") + t.string + end + + + def numenc(x) + if ((x.nan? || x.infinite?) rescue false) + raise Error, "Numeric cannot be represented: #{x}" + end + "#{x}" + end + + + # Copies the valid UTF-8 bytes of a single character + # from string s at position i to I/O object t, and + # returns the number of bytes copied. + # If no valid UTF-8 char exists at position i, + # ucharcopy writes Ustrerr and returns 1. + def ucharcopy(t, s, i) + n = s.length - i + raise Utf8Error if n < 1 + + c0 = s[i].ord + + # 1-byte, 7-bit sequence? + if c0 < Utagx + t.putc(c0) + return 1 + end + + raise Utf8Error if c0 < Utag2 # unexpected continuation byte? + + raise Utf8Error if n < 2 # need continuation byte + c1 = s[i+1].ord + raise Utf8Error if c1 < Utagx || Utag2 <= c1 + + # 2-byte, 11-bit sequence? + if c0 < Utag3 + raise Utf8Error if ((c0&Umask2)<<6 | (c1&Umaskx)) <= Uchar1max + t.putc(c0) + t.putc(c1) + return 2 + end + + # need second continuation byte + raise Utf8Error if n < 3 + + c2 = s[i+2].ord + raise Utf8Error if c2 < Utagx || Utag2 <= c2 + + # 3-byte, 16-bit sequence? + if c0 < Utag4 + u = (c0&Umask3)<<12 | (c1&Umaskx)<<6 | (c2&Umaskx) + raise Utf8Error if u <= Uchar2max + t.putc(c0) + t.putc(c1) + t.putc(c2) + return 3 + end + + # need third continuation byte + raise Utf8Error if n < 4 + c3 = s[i+3].ord + raise Utf8Error if c3 < Utagx || Utag2 <= c3 + + # 4-byte, 21-bit sequence? + if c0 < Utag5 + u = (c0&Umask4)<<18 | (c1&Umaskx)<<12 | (c2&Umaskx)<<6 | (c3&Umaskx) + raise Utf8Error if u <= Uchar3max + t.putc(c0) + t.putc(c1) + t.putc(c2) + t.putc(c3) + return 4 + end + + raise Utf8Error + rescue Utf8Error + t.write(Ustrerr) + return 1 + end + + + class Utf8Error < ::StandardError + end + + + class Error < ::StandardError + end + + + Utagx = 0x80 # 1000 0000 + Utag2 = 0xc0 # 1100 0000 + Utag3 = 0xe0 # 1110 0000 + Utag4 = 0xf0 # 1111 0000 + Utag5 = 0xF8 # 1111 1000 + Umaskx = 0x3f # 0011 1111 + Umask2 = 0x1f # 0001 1111 + Umask3 = 0x0f # 0000 1111 + Umask4 = 0x07 # 0000 0111 + Uchar1max = (1<<7) - 1 + Uchar2max = (1<<11) - 1 + Uchar3max = (1<<16) - 1 + Ucharerr = 0xFFFD # unicode "replacement char" + Ustrerr = "\xef\xbf\xbd" # unicode "replacement char" + Usurrself = 0x10000 + Usurr1 = 0xd800 + Usurr2 = 0xdc00 + Usurr3 = 0xe000 + + Spc = ' '[0] + Unesc = {?b=>?\b, ?f=>?\f, ?n=>?\n, ?r=>?\r, ?t=>?\t} + end +end From 4349bb18e8f91d60fe4d223a54e032dd02d796ca Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 29 Jan 2015 11:26:08 -0800 Subject: [PATCH 292/952] prevent fork from deleting existing apps --- lib/heroku/command/fork.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index e12e51bc0..6b0855619 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -24,6 +24,11 @@ def index raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") end + begin + api.get_app(to).body + error "#{to} app exists.\nUSAGE: heroku fork -a COPY_FROM COPY_TO" + rescue + end from_info = api.get_app(from).body to_info = action("Creating fork #{to}", :org => !!org) do @@ -34,7 +39,7 @@ def index "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] } - info = if org + if org org_api.post_app(params, org).body else api.post_app(params).body From afbe8996ae6b84fc3200569b81e36ec79e36f9f5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 29 Jan 2015 10:57:01 -0800 Subject: [PATCH 293/952] show app name in run:detached command Fixes #1390 --- lib/heroku/command/run.rb | 2 +- spec/heroku/command/run_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 32be316f2..0ba73a8ec 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -72,7 +72,7 @@ def detached log_displayer = ::Heroku::Helpers::LogDisplayer.new(heroku, app, opts) log_displayer.display_logs else - display("Use `heroku logs -p #{process_data['process']}` to view the output.") + display("Use `heroku logs -p #{process_data['process']} -a #{app_name}` to view the output.") end end diff --git a/spec/heroku/command/run_spec.rb b/spec/heroku/command/run_spec.rb index 966419faa..ba47a7c75 100644 --- a/spec/heroku/command/run_spec.rb +++ b/spec/heroku/command/run_spec.rb @@ -34,7 +34,7 @@ expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Running `bin/foo` detached... up, run.1 -Use `heroku logs -p run.1` to view the output. +Use `heroku logs -p run.1 -a example` to view the output. STDOUT end From e249f6ffa24873b62e387f780ab7766bf2dccc2d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 29 Jan 2015 15:58:42 -0800 Subject: [PATCH 294/952] added uninstalling of jsplugins --- lib/heroku/command/plugins.rb | 9 ++++++--- lib/heroku/jsplugin.rb | 4 ++++ spec/heroku/command/plugins_spec.rb | 27 --------------------------- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 3c3e62ccc..76edd8120 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -62,9 +62,12 @@ def install def uninstall plugin = Heroku::Plugin.new(shift_argument) validate_arguments! - - action("Uninstalling #{plugin.name}") do - plugin.uninstall + if Heroku::Plugin.list.include? plugin.name + action("Uninstalling #{plugin.name}") do + plugin.uninstall + end + elsif Heroku::JSPlugin.setup? + Heroku::JSPlugin.uninstall(plugin.name) end end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index b81c0c261..7aa0c070d 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -67,6 +67,10 @@ def self.install(name) system "#{bin} plugins:install #{name}" end + def self.uninstall(name) + system "#{bin} plugins:uninstall #{name}" + end + def self.version `#{bin} version` end diff --git a/spec/heroku/command/plugins_spec.rb b/spec/heroku/command/plugins_spec.rb index 40263dcf1..ef39d23ed 100644 --- a/spec/heroku/command/plugins_spec.rb +++ b/spec/heroku/command/plugins_spec.rb @@ -36,33 +36,6 @@ module Heroku::Command end - context("uninstall") do - - before do - expect(Heroku::Plugin).to receive(:new).with('Plugin').and_return(@plugin) - end - - it "uninstalls plugins" do - expect(@plugin).to receive(:uninstall).and_return(true) - stderr, stdout = execute("plugins:uninstall Plugin") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Uninstalling Plugin... done -STDOUT - end - - it "does not uninstall plugins that do not exist" do - stderr, stdout = execute("plugins:uninstall Plugin") - expect(stderr).to eq <<-STDERR - ! Plugin plugin not found. -STDERR - expect(stdout).to eq <<-STDOUT -Uninstalling Plugin... failed -STDOUT - end - - end - context("update") do before do From 4e9434a3af7f5372b030c9431496631f81784ce9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 29 Jan 2015 16:11:04 -0800 Subject: [PATCH 295/952] v3.25.0 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c52c47c89..bf2d0d410 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.25.0 2015-01-29 +================= +Added `plugins:uninstall` for toolbelt v4 plugins +Prevent fork from deleting an existing app +Added okjson back in for users that have not updated their plugins +Show app name in `run:detached` + 3.24.5 2015-01-28 ================= Better errors when git is not found or not working diff --git a/Gemfile.lock b/Gemfile.lock index dd93a94ee..cc13bfbd7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.24.5) + heroku (3.25.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d4bf0875a..c11a7cd94 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.24.5" + VERSION = "3.25.0" end From 9e86213586d188163b12ef24fc5336b67bd39c47 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 30 Jan 2015 08:58:08 -0800 Subject: [PATCH 296/952] add out of date warning for non-autoupdating clients --- lib/heroku/updater.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 61420d5a2..15a14cb5d 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -87,7 +87,7 @@ def self.wait_for_lock(wait_for=5, check_every=0.5) end def self.autoupdate - return if disable + return warn_if_out_of_date if disable # if we've updated in the last hour, don't try again if File.exists?(last_autoupdate_path) return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60 @@ -97,6 +97,10 @@ def self.autoupdate update end + def self.warn_if_out_of_date + $stderr.puts "WARNING: Toolbelt v#{latest_version} update available." if needs_update? + end + def self.update(prerelease=false) return unless prerelease || needs_update? From 22003669ee9f59681b61887775925ecbe3e21059 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 30 Jan 2015 09:37:20 -0800 Subject: [PATCH 297/952] remove default org functionality in favor of HEROKU_ORGANIZATION --- lib/heroku/client/organizations.rb | 18 ------- lib/heroku/command/base.rb | 19 +++----- lib/heroku/command/orgs.rb | 30 ++---------- spec/heroku/command/apps_spec.rb | 16 ++----- spec/heroku/command/orgs_spec.rb | 76 +++--------------------------- 5 files changed, 19 insertions(+), 140 deletions(-) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 86b9b42c2..31d21fc6f 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -84,24 +84,6 @@ def get_orgs end end - def remove_default_org - api.request( - :expects => 204, - :method => :delete, - :path => "/v1/user/default-organization" - ) - end - - def set_default_org(org) - api.request( - :expects => 200, - :method => :post, - :path => "/v1/user/default-organization", - :body => Heroku::Helpers.json_encode( { "default_organization" => org } ), - :headers => {"Content-Type" => "application/json"} - ) - end - # Apps ################################# def get_apps(org) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index c576fb628..8f866a958 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -47,20 +47,13 @@ def org options[:org] elsif options[:personal] || @nil nil - elsif org_from_app = extract_org_from_app - org_from_app + elsif ENV['HEROKU_ORGANIZATION'] + ENV['HEROKU_ORGANIZATION'] + elsif options[:ignore_no_org] + nil else - response = org_api.get_orgs.body - default = response['user']['default_organization'] - if default - options[:using_default_org] = true - default - elsif options[:ignore_no_org] - nil - else - # raise instead of using error command to enable rescuing when app is optional - raise Heroku::Command::CommandFailed.new("No org specified.\nRun this command from an app folder which belongs to an org or specify which org to use with --org ORG.") - end + # raise instead of using error command to enable rescuing when app is optional + raise Heroku::Command::CommandFailed.new("No org specified.\nRun this command from an app folder which belongs to an org or specify which org to use with --org ORG.") end @nil = true if @org == nil diff --git a/lib/heroku/command/orgs.rb b/lib/heroku/command/orgs.rb index 5800b3750..5ed5d9f03 100644 --- a/lib/heroku/command/orgs.rb +++ b/lib/heroku/command/orgs.rb @@ -22,13 +22,10 @@ def index end end - default = response['user']['default_organization'] || "" - orgs.map! do |org| name = org["organization_name"] t = [] t << org["role"] - t << 'default' if name == default [name, t.join(', ')] end @@ -48,33 +45,12 @@ def open launchy("Opening web interface for #{org}", "https://dashboard.heroku.com/orgs/#{org}/apps") end - # orgs:default [TARGET] - # - # sets the default org. - # TARGET can be an org you belong to or it can be "personal" - # for your personal account. If no argument or option is given, - # the default org is displayed + # orgs:default # + # DEPRECATED: Use HEROKU_ORGANIZATION environment variable # def default - options[:ignore_no_org] = true - if target = shift_argument - options[:org] = target - end - - if org == "personal" || options[:personal] - action("Setting personal account as default") do - org_api.remove_default_org - end - elsif org && !options[:using_default_org] - action("Setting #{org} as the default organization") do - org_api.set_default_org(org) - end - elsif org - display("#{org} is the default organization.") - else - display("Personal account is default.") - end + display("DEPRECATED: Use HEROKU_ORGANIZATION environment variable.") end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 250fa95a2..9fa570e0b 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -189,19 +189,9 @@ module Heroku::Command context("index with orgs") do context("when you are a member of the org") do - before(:each) do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, { :status => 200, :body => MultiJson.dump({ - "user" => {"default_organization" => "test-org"} - })}) - end - - after(:each) do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, { :status => 404 }) - end - it "displays a message when the org has no apps" do Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }, { :status => 200, :body => MultiJson.dump([]) }) - stderr, stdout = execute("apps") + stderr, stdout = execute("apps -o test-org") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT There are no apps in organization test-org. @@ -223,7 +213,7 @@ module Heroku::Command end it "lists joined apps in an organization" do - stderr, stdout = execute("apps") + stderr, stdout = execute("apps -o test-org") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === Apps joined in organization test-org @@ -233,7 +223,7 @@ module Heroku::Command end it "list all apps in an organization with the --all flag" do - stderr, stdout = execute("apps --all") + stderr, stdout = execute("apps --all -o test-org") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === Apps joined in organization test-org diff --git a/spec/heroku/command/orgs_spec.rb b/spec/heroku/command/orgs_spec.rb index e39321638..27ccf6a5e 100644 --- a/spec/heroku/command/orgs_spec.rb +++ b/spec/heroku/command/orgs_spec.rb @@ -42,7 +42,7 @@ module Heroku::Command it "labels a user's default organization" do Excon.stub({ :method => :get, :path => '/v1/user/info' }, { - :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}], "user" => {"default_organization" => "test-org2"}}), + :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org", "role" => "collaborator"}, {"organization_name" => "test-org2", "role" => "admin"}]}), :status => 200 } ) @@ -51,66 +51,9 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT test-org collaborator -test-org2 admin, default - -STDOUT - end - end - - context(:default) do - context "when a target org is specified" do - it "sets the default org to the target" do - expect(org_api).to receive(:set_default_org).with("test-org").once - stderr, stdout = execute("orgs:default test-org") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Setting test-org as the default organization... done -STDOUT - end - - it "removes the default org when the org name is 'personal'" do - expect(org_api).to receive(:remove_default_org).once - stderr, stdout = execute("orgs:default personal") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Setting personal account as default... done -STDOUT - end - - it "removes the defautl org when the personal flag is passed" do - expect(org_api).to receive(:remove_default_org).once - stderr, stdout = execute("orgs:default --personal") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Setting personal account as default... done -STDOUT - end - - end - - context "when no target is specified" do - it "displays the default organization when present" do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, - { - :body => MultiJson.dump({"user" => {"default_organization" => "test-org"}}), - :status => 200 - } - ) - - stderr, stdout = execute("orgs:default") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -test-org is the default organization. -STDOUT - end +test-org2 admin - it "displays personal account as default when no org present" do - stderr, stdout = execute("orgs:default") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Personal account is default. STDOUT - end end end @@ -121,24 +64,19 @@ module Heroku::Command end it "opens the org specified in an argument" do - stderr, stdout = execute("orgs:open --org test-org") + _, stdout = execute("orgs:open --org test-org") expect(stdout).to eq <<-STDOUT Opening web interface for test-org... done STDOUT end - it "opens the default org" do - Excon.stub({ :method => :get, :path => '/v1/user/info' }, - { - :body => MultiJson.dump({"organizations" => [{"organization_name" => "test-org"}], "user" => {"default_organization" => "test-org"}}), - :status => 200 - } - ) - - stderr, stdout = execute("orgs:open") + it "opens the org specified in HEROKU_ORGANIZATION" do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + _, stdout = execute("orgs:open") expect(stdout).to eq <<-STDOUT Opening web interface for test-org... done STDOUT + ENV['HEROKU_ORGANIZATION'] = nil end end end From 7ed6fffbd84c3167268ec9f29e83f24d6c69f00b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 30 Jan 2015 16:29:51 -0800 Subject: [PATCH 298/952] warn when using jruby with heroku run commands --- lib/heroku/command/run.rb | 1 + lib/heroku/helpers.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 0ba73a8ec..0acd083e6 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -37,6 +37,7 @@ class Heroku::Command::Run < Heroku::Command::Base def index command = args.join(" ") error("Usage: heroku run COMMAND") if command.empty? + warn_if_using_jruby run_attached(command) end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index e1bedc759..9653d47b6 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -558,5 +558,9 @@ def app_owner email def has_http_git_entry_in_netrc Auth.netrc && Auth.netrc[Auth.http_git_host] end + + def warn_if_using_jruby + stderr_puts "WARNING: jruby is known to cause issues when used with the toolbelt." if RUBY_PLATFORM == "java" + end end end From 99f6f97148d5ff39017033a7c01dfbacf3fd5204 Mon Sep 17 00:00:00 2001 From: Mary Brennan Date: Mon, 2 Feb 2015 10:43:19 -0800 Subject: [PATCH 299/952] Update two_factor.rb Moved the help comment next to the class definition so that it will show up in the CLI help when users type heroku help. Also edited the CLI help text. --- lib/heroku/command/two_factor.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb index 447c89dcb..2d3ede5ad 100644 --- a/lib/heroku/command/two_factor.rb +++ b/lib/heroku/command/two_factor.rb @@ -1,12 +1,12 @@ require "heroku/command/base" -# manage two factor settings for account -# module Heroku::Command + # manage two-factor authentication settings for your account + # class TwoFactor < BaseWithApp # 2fa # - # Display whether two-factor is enabled or not + # Display whether two-factor authentication is enabled or not # def index account = api.request( @@ -16,9 +16,9 @@ def index :path => "/account").body if account["two_factor_authentication"] - display "Two-factor auth is enabled." + display "Two-factor authentication is enabled." else - display "Two-factor is not enabled." + display "Two-factor authentication is not enabled." end end @@ -26,7 +26,7 @@ def index # 2fa:disable # - # Disable 2fa on your account + # Disable two-factor authentication for your account # def disable print "Password (typing will be hidden): " @@ -52,7 +52,7 @@ def disable # 2fa:generate-recovery-codes # - # Generates (and replaces) recovery codes + # Generates and replaces recovery codes # def generate_recovery_codes code = Heroku::Auth.ask_for_second_factor From df797d9b703945c771c65554f5b6903334a2ebf4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 2 Feb 2015 14:38:00 -0800 Subject: [PATCH 300/952] update travis rubies 1.9.2 is flaky and used by almost nobody Also added 2.2.x and updated 2.1.x to the latest. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1e830c789..1127b0f00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,10 @@ language: ruby rvm: - 1.8.7 - - 1.9.2 - 1.9.3 - 2.0.0 - - 2.1.2 + - 2.1.5 + - 2.2.0 sudo: false From 722ac054533eb0687b4037da928bbf9a3d328bb3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 2 Feb 2015 16:21:22 -0800 Subject: [PATCH 301/952] update 2fa help text --- lib/heroku/command/two_factor.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb index 2d3ede5ad..84bcd2270 100644 --- a/lib/heroku/command/two_factor.rb +++ b/lib/heroku/command/two_factor.rb @@ -1,10 +1,10 @@ require "heroku/command/base" module Heroku::Command - # manage two-factor authentication settings for your account + # manage two-factor authentication settings # class TwoFactor < BaseWithApp - # 2fa + # twofactor # # Display whether two-factor authentication is enabled or not # @@ -24,7 +24,7 @@ def index alias_command "2fa", "twofactor" - # 2fa:disable + # twofactor:disable # # Disable two-factor authentication for your account # @@ -50,7 +50,7 @@ def disable alias_command "2fa:disable", "twofactor:disable" - # 2fa:generate-recovery-codes + # twofactor:generate-recovery-codes # # Generates and replaces recovery codes # From 1eda002820ef356a97ae2049373f8a2ea48fb58e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 3 Feb 2015 10:56:25 -0800 Subject: [PATCH 302/952] Replace plugin examples with heroku-production-check Moving towards a jsplugin as an example plugin as well as removing the current example of heroku-accounts (which has many issues) --- lib/heroku/command/plugins.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 76edd8120..7bdfe0820 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -13,7 +13,7 @@ class Plugins < Base # # $ heroku plugins # === Installed Plugins - # heroku-accounts + # heroku-production-check@0.2.0 # def index validate_arguments! @@ -35,8 +35,8 @@ def index # #Example: # - # $ heroku plugins:install https://github.com/ddollar/heroku-accounts.git - # Installing heroku-accounts... done + # $ heroku plugins:install heroku-production-check + # Installing heroku-production-check... done # def install name = shift_argument @@ -56,8 +56,8 @@ def install # #Example: # - # $ heroku plugins:uninstall heroku-accounts - # Uninstalling heroku-accounts... done + # $ heroku plugins:uninstall heroku-production-check + # Uninstalling heroku-production-check... done # def uninstall plugin = Heroku::Plugin.new(shift_argument) @@ -78,10 +78,10 @@ def uninstall #Example: # # $ heroku plugins:update - # Updating heroku-accounts... done + # Updating heroku-production-check... done # - # $ heroku plugins:update heroku-accounts - # Updating heroku-accounts... done + # $ heroku plugins:update heroku-production-check + # Updating heroku-production-check... done # def update plugins = if plugin = shift_argument From 31e448cc2ffc0ab7fbe9473e214a71267c3cd53a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 3 Feb 2015 11:11:03 -0800 Subject: [PATCH 303/952] Added a better error message for heroku run ssl errors --- lib/heroku/command/run.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 0ba73a8ec..203c0e40d 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -149,7 +149,7 @@ def rendezvous_session(rendezvous_url, &on_connect) rescue Timeout::Error, Errno::ETIMEDOUT error "\nTimeout awaiting dyno, see https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process" rescue OpenSSL::SSL::SSLError - error "Authentication error" + error "\nSSL error connecting to dyno." rescue Errno::ECONNREFUSED, Errno::ECONNRESET error "\nError connecting to dyno, see https://devcenter.heroku.com/articles/one-off-dynos#timeout-awaiting-process" rescue Interrupt From afb4dcfac15c63c27bd53f2abf64ec5fbcbf5fa0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 4 Feb 2015 15:23:19 -0800 Subject: [PATCH 304/952] create error log dir if it does not exist --- lib/heroku/helpers.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 9653d47b6..6051c86d0 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -444,6 +444,7 @@ def styled_error(error, message='Heroku client internal error.') end def error_log(*obj) + FileUtils.mkdir_p(File.dirname(error_log_path)) File.open(error_log_path, 'a') do |file| file.write(obj.join("\n") + "\n") end From 5c65f8c95e4e540f12700408f137c51f83aba6d9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 4 Feb 2015 15:23:51 -0800 Subject: [PATCH 305/952] show better error message when error reading netrc --- lib/heroku/auth.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index bbe30408a..ffce59e1a 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -141,11 +141,13 @@ def netrc # :nodoc: @netrc ||= begin File.exists?(netrc_path) && Netrc.read(netrc_path) rescue => error - if error.message =~ /^Permission bits for/ - perm = File.stat(netrc_path).mode & 0777 - abort("Permissions #{perm} for '#{netrc_path}' are too open. You should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.") + case error.message + when /^Permission bits for/ + abort("#{error.message}.\nYou should run `chmod 0600 #{netrc_path}` so that your credentials are NOT accessible by others.") + when /EACCES/ + error("Error reading #{netrc_path}\n#{error.message}\nMake sure this user can read/write this file.") else - raise error + error("Error reading #{netrc_path}\n#{error.message}\nYou may need to delete this file and run `heroku login` to recreate it.") end end end From 47990b725bddcf51b0dafad25d92c93ae7a1b265 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Fri, 26 Dec 2014 13:46:32 -0600 Subject: [PATCH 306/952] Added a buildpack command for setting and unsetting buildpack urls as first class attributes of an App. This new command requires the v3 API for app. As a result an app_v3.rb file has been created in the same pattern as used by the fork command. Added an index method for buildpack command. This command displays the currently set buildpack URL or displays a message that no buildpack URL has been set. It uses the PAI v3 endpoint as with the set and unset methods. Updated to use new API for buildpack-installations --- lib/heroku/api/apps_v3.rb | 27 +++++++++ lib/heroku/command/buildpack.rb | 56 +++++++++++++++++ spec/heroku/command/buildpack_spec.rb | 86 +++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 lib/heroku/api/apps_v3.rb create mode 100644 lib/heroku/command/buildpack.rb create mode 100644 spec/heroku/command/buildpack_spec.rb diff --git a/lib/heroku/api/apps_v3.rb b/lib/heroku/api/apps_v3.rb new file mode 100644 index 000000000..b0e865845 --- /dev/null +++ b/lib/heroku/api/apps_v3.rb @@ -0,0 +1,27 @@ +module Heroku + class API + def get_app_buildpacks_v3(app) + headers = { 'Accept' => 'application/vnd.heroku+json; version=3' } + request( + :expects => [ 200, 206 ], + :headers => headers, + :method => :get, + :path => "/apps/#{app}/buildpack-installations" + ) + end + + def put_app_buildpacks_v3(app, body={}) + headers = { + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Content-Type' => 'application/json' + } + request( + :expects => 200, + :headers => headers, + :method => :put, + :path => "/apps/#{app}/buildpack-installations", + :body => Heroku::Helpers.json_encode(body) + ) + end + end +end diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb new file mode 100644 index 000000000..9a68563ce --- /dev/null +++ b/lib/heroku/command/buildpack.rb @@ -0,0 +1,56 @@ +require "heroku/command/base" +require "heroku/api/apps_v3" + +module Heroku::Command + + # manage the buildpack for an app + # + class Buildpack < Base + + # buildpack + # + # display the buildpack_url for an app + # + #Examples: + # + # $ heroku buildpack + # https://github.com/heroku/heroku-buildpack-ruby + # + def index + validate_arguments! + + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + if app_buildpacks.nil? or app_buildpacks.empty? + display("#{app} has no Buildpack URL set.") + else + styled_header("#{app} Buildpack URL") + display(app_buildpacks.first["buildpack"]["url"]) + end + end + + # buildpack:set BUILDPACK_URL + # + # set new app buildpack + # + def set + unless buildpack_url = shift_argument + error("Usage: heroku buildpack:set BUILDPACK_URL.\nMust specify target buildpack URL.") + end + + api.put_app_buildpacks_v3(app, {updates: [{buildpack: buildpack_url}]}) + display "Buildpack set. Next release on #{app} will use #{buildpack_url}." + display "Run `git push heroku master` to create a new release on #{buildpack_url}." + end + + # buildpack:unset + # + # unset the app buildpack + # + def unset + api.put_app_buildpacks_v3(app, {updates: []}) + display "Buildpack unset. Next release on #{app} will detect buildpack normally." + end + + end +end diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb new file mode 100644 index 000000000..1b9311253 --- /dev/null +++ b/spec/heroku/command/buildpack_spec.rb @@ -0,0 +1,86 @@ +require "spec_helper" +require "heroku/command/buildpack" + +module Heroku::Command + describe Buildpack do + + before(:each) do + stub_core + api.post_app("name" => "example", "stack" => "cedar-14") + + Excon.stub({:method => :put, :path => "/apps/example/buildpack-installations"}, + {:status => 200}) + Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, + { + :body => [{"buildpack" => { "url" => "https://github.com/heroku/heroku-buildpack-ruby"}}], + :status => 200 + }) + end + + after(:each) do + Excon.stubs.shift + Excon.stubs.shift + api.delete_app("example") + end + + describe "index" do + it "displays the buildpack URL" do + stderr, stdout = execute("buildpack") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URL +https://github.com/heroku/heroku-buildpack-ruby + STDOUT + end + + context "with no buildpack URL set" do + before(:each) do + Excon.stubs.shift + Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, + { + :body => [], + :status => 200 + }) + end + + it "does not display a buildpack URL" do + stderr, stdout = execute("buildpack") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +example has no Buildpack URL set. + STDOUT + end + end + end + + describe "set" do + it "sets the buildpack URL" do + stderr, stdout = execute("buildpack:set https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release on https://github.com/heroku/heroku-buildpack-ruby. + STDOUT + end + + it "handles a missing buildpack URL arg" do + stderr, stdout = execute("buildpack:set") + expect(stderr).to eq <<-STDERR + ! Usage: heroku buildpack:set BUILDPACK_URL. + ! Must specify target buildpack URL. + STDERR + expect(stdout).to eq("") + end + end + + describe "unset" do + it "unsets the buildpack URL" do + stderr, stdout = execute("buildpack:unset") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack unset. Next release on example will detect buildpack normally. + STDOUT + end + end + end +end From 6e9ebc82252b3cb5fae99520a3177e1c96f27dd6 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Thu, 5 Feb 2015 14:01:31 -0600 Subject: [PATCH 307/952] Added hash rockets to buildpack command for ruby 1.8 support --- lib/heroku/command/buildpack.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 9a68563ce..0b05dde5b 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -38,7 +38,7 @@ def set error("Usage: heroku buildpack:set BUILDPACK_URL.\nMust specify target buildpack URL.") end - api.put_app_buildpacks_v3(app, {updates: [{buildpack: buildpack_url}]}) + api.put_app_buildpacks_v3(app, {:updates => [{:buildpack => buildpack_url}]}) display "Buildpack set. Next release on #{app} will use #{buildpack_url}." display "Run `git push heroku master` to create a new release on #{buildpack_url}." end @@ -48,7 +48,7 @@ def set # unset the app buildpack # def unset - api.put_app_buildpacks_v3(app, {updates: []}) + api.put_app_buildpacks_v3(app, {:updates => []}) display "Buildpack unset. Next release on #{app} will detect buildpack normally." end From 2d79e8f422780549fef62dd5406c0f38f6cd0056 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Fri, 6 Feb 2015 09:24:57 -0600 Subject: [PATCH 308/952] Changed wording of output after setting a buildpack URL. --- lib/heroku/command/buildpack.rb | 2 +- spec/heroku/command/buildpack_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 0b05dde5b..1b74435ce 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -40,7 +40,7 @@ def set api.put_app_buildpacks_v3(app, {:updates => [{:buildpack => buildpack_url}]}) display "Buildpack set. Next release on #{app} will use #{buildpack_url}." - display "Run `git push heroku master` to create a new release on #{buildpack_url}." + display "Run `git push heroku master` to create a new release using #{buildpack_url}." end # buildpack:unset diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 1b9311253..1112b2546 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -59,7 +59,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. -Run `git push heroku master` to create a new release on https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. STDOUT end From 546f9875cfba9ebed0dcc1ad335b02fcb09bec2b Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Fri, 6 Feb 2015 10:07:56 -0600 Subject: [PATCH 309/952] Added a warning on buildpack:unset if BUILDPACK_URL or LANGUAGE_PACK_URL config vars are set. --- lib/heroku/command/buildpack.rb | 12 +++++++++++- spec/heroku/command/buildpack_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 1b74435ce..4f23402f5 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -49,7 +49,17 @@ def set # def unset api.put_app_buildpacks_v3(app, {:updates => []}) - display "Buildpack unset. Next release on #{app} will detect buildpack normally." + + vars = api.get_config_vars(app).body + if vars.has_key?("BUILDPACK_URL") + display "Buildpack unset." + warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" + elsif vars.has_key?("LANGUAGE_PACK_URL") + display "Buildpack unset." + warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" + else + display "Buildpack unset. Next release on #{app} will detect buildpack normally." + end end end diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 1112b2546..f2fddfd3e 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -81,6 +81,28 @@ module Heroku::Command Buildpack unset. Next release on example will detect buildpack normally. STDOUT end + + it "unsets and warns about buildpack URL config var" do + execute("config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:unset") + expect(stderr).to eq <<-STDERR +WARNING: The BUILDPACK_URL config var is still set and will be used for the next release + STDERR + expect(stdout).to eq <<-STDOUT +Buildpack unset. + STDOUT + end + + it "unsets and warns about language pack URL config var" do + execute("config:set LANGUAGE_PACK_URL=https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:unset") + expect(stderr).to eq <<-STDERR +WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release + STDERR + expect(stdout).to eq <<-STDOUT +Buildpack unset. + STDOUT + end end end end From fdca27d45025654552928eae35632d8e576651b9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Feb 2015 12:46:13 -0800 Subject: [PATCH 310/952] fix raising of rollbar errors to show the http response --- lib/heroku/rollbar.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index 08117ddc6..3be30a100 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -6,7 +6,7 @@ def self.error(e) payload = json_encode(build_payload(e)) response = Excon.post('https://api.rollbar.com/api/1/item/', :body => payload) response = json_decode(response.body) - raise response if response["err"] != 0 + raise response.to_s if response["err"] != 0 response["result"]["uuid"] rescue => e $stderr.puts "Error submitting error." From b1685768d851780012d112c641bb74e1454a125d Mon Sep 17 00:00:00 2001 From: Matt Gauger Date: Fri, 6 Feb 2015 17:09:12 -0600 Subject: [PATCH 311/952] Header name is a string - This prevented the confirmation dialog from working: https://github.com/heroku/heroku-addon-attachments/issues/25 --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 53859fa06..3d7bb00f4 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -235,7 +235,7 @@ def self.run(cmd, arguments=[]) e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" } rescue Heroku::API::Errors::Locked => e - app = e.response.headers[:x_confirmation_required] + app = e.response.headers["X-Confirmation-Required"] if confirm_command(app, extract_error(e.response.body)) arguments << '--confirm' << app retry From c2e5f68e532f9539ba742dea1949a8ffd7ba410c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Feb 2015 16:44:46 -0800 Subject: [PATCH 312/952] updated jsplugin url --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 7aa0c070d..ee8d9f3c6 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -117,7 +117,7 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/heroku-cli/master/manifest.json").body) + @manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/master/manifest.json").body) end def self.url From d2cbf77d86ec91f373a749a3e7f776952d1e854e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Feb 2015 17:09:59 -0800 Subject: [PATCH 313/952] use heroku-cli.exe for windows --- lib/heroku/jsplugin.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index ee8d9f3c6..7c90eaa6c 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -76,7 +76,11 @@ def self.version end def self.bin - File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") + if os == 'windows' + File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli.exe") + else + File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") + end end def self.setup From dbd7ad0a24287431c986544861d9a0c482dab4fb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Feb 2015 17:12:09 -0800 Subject: [PATCH 314/952] use topic only if no command for v4 commands --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 7c90eaa6c..f93939c37 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -26,7 +26,7 @@ def initialize(args, opts) exec this.bin, "#{plugin['topic']}:#{plugin['command']}", *@args end Heroku::Command.register_command( - :command => "#{plugin['topic']}:#{plugin['command']}", + :command => plugin['command'] ? "#{plugin['topic']}:#{plugin['command']}" : plugin['topic'], :namespace => plugin['topic'], :klass => klass, :method => :run, From 01ae45952d2ab22f3533986527a21e55b492fff0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Feb 2015 17:54:38 -0800 Subject: [PATCH 315/952] show errors rollbar does not take --- lib/heroku/rollbar.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index 3be30a100..f0dc1849b 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -9,8 +9,7 @@ def self.error(e) raise response.to_s if response["err"] != 0 response["result"]["uuid"] rescue => e - $stderr.puts "Error submitting error." - error_log(e.message, e.backtrace.join("\n")) + $stderr.puts(e.message, e.backtrace.join("\n")) nil end From cc1e672472f0c1ecb16ce27ff06a8d5c645ac7b4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Feb 2015 17:56:20 -0800 Subject: [PATCH 316/952] show errors rollbar does not take --- lib/heroku/rollbar.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index f0dc1849b..ef26c9e44 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -8,7 +8,7 @@ def self.error(e) response = json_decode(response.body) raise response.to_s if response["err"] != 0 response["result"]["uuid"] - rescue => e + rescue $stderr.puts(e.message, e.backtrace.join("\n")) nil end From f3f33f7b8a4f44c10214bcfe448f62da7750c2ad Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Mon, 9 Feb 2015 18:40:47 +0000 Subject: [PATCH 317/952] switched regex to match pg:backups unschedule color --- lib/heroku/command/pg_backups.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 50bfee4cc..6fa0d33d5 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -397,7 +397,9 @@ def unschedule_backups attachment = generate_resolver.resolve(db, "DATABASE_URL") schedule = hpg_client(attachment).schedules.find do |s| - attachment.name =~ /#{s[:name]}/ + # attachment.name is HEROKU_POSTGRESQL_COLOR + # s[:name] is HEROKU_POSTGRESQL_COLOR_URL + /#{attachment.name}/ =~ s[:name] end if schedule.nil? From a7e1207a12d569f5d78871062ba164caae8981de Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Tue, 10 Feb 2015 05:08:51 +0000 Subject: [PATCH 318/952] added checks both ways --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 6fa0d33d5..1edb1f90e 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -399,7 +399,7 @@ def unschedule_backups schedule = hpg_client(attachment).schedules.find do |s| # attachment.name is HEROKU_POSTGRESQL_COLOR # s[:name] is HEROKU_POSTGRESQL_COLOR_URL - /#{attachment.name}/ =~ s[:name] + s[:name] =~ /#{attachment.name}/ || attachment.name =~ /#{s[:name]}/ end if schedule.nil? From bb6b9cdd8cde32eed55d75cdf81d493400125f1e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 10 Feb 2015 17:08:39 -0800 Subject: [PATCH 319/952] v3.26.0 --- CHANGELOG | 17 +++++++++++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bf2d0d410..6b57d9710 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,20 @@ +3.26.0 2015-02-10 +================= +Removed default orgs in place of HEROKU_ORGANIZATION env var (#1395) +Display errors if rollbar does not accept it (#1412) +Fix case-sensitive reading of X-Confirmation-Required header (#1410) +Fix v4 plugin commands without command name and topic only +Bug fixes for v4 plugins on Windows +Show rollbar errors in ~/.heroku/error.log (#1408) +Allow db:push and db:pull to work with remote databases (#1386) +Cleaner error messages when failing to read netrc files (#1404) +Create heroku directory if it does not exist when writing ~/.heroku/error.log (#1403) +More descriptive error message when heroku run has an SSL error (#1401) +Change plugin example to use heroku-production-check instead of heroku-accounts (#1400) +Updated help text for twofactor commands (#1398) +Show warning if CLI is run under jruby (#1396) +Show out of date warning if toolbelt is not autoupdatable (#1394) + 3.25.0 2015-01-29 ================= Added `plugins:uninstall` for toolbelt v4 plugins diff --git a/Gemfile.lock b/Gemfile.lock index cc13bfbd7..ab623e154 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.25.0) + heroku (3.26.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index c11a7cd94..cad234125 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.25.0" + VERSION = "3.26.0" end From f194b0b1118ac3ab61dd9a3b955a561d4fabea1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimir=20T=C3=A1mara=20Pati=C3=B1o?= Date: Wed, 11 Feb 2015 06:44:53 -0500 Subject: [PATCH 320/952] Update jsplugin.rb --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index f93939c37..05550f7de 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -115,6 +115,8 @@ def self.os "linux" when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ "windows" + when /openbsd/ + "openbsd" else raise "unsupported on #{RUBY_PLATFORM}" end From 80ebe949f4433bbf8a5d43961e79175ee19a6730 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 11 Feb 2015 10:28:34 -0800 Subject: [PATCH 321/952] made pg:pull help clearer fixes #1416 --- lib/heroku/command/pg.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index c206aa8a5..5a97052a3 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -338,9 +338,8 @@ def push # pull from REMOTE_SOURCE_DATABASE to TARGET_DATABASE # TARGET_DATABASE must not already exist. # - # TARGET_DATABASE must be either the name of a database - # existing on your localhost or the fully qualified URL of - # a remote database. + # TARGET_DATABASE will be created locally if it's a database name + # or remotely if it's a fully qualified URL. def pull requires_preauth remote, local = shift_argument, shift_argument From 8604f57408a0b73fb2a0f804775b3f5390ed3fe8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 11 Feb 2015 14:02:20 -0800 Subject: [PATCH 322/952] make sure HEROKU_ORGANIZATION is not empty --- lib/heroku/command/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 8f866a958..5e5e84d09 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -47,7 +47,7 @@ def org options[:org] elsif options[:personal] || @nil nil - elsif ENV['HEROKU_ORGANIZATION'] + elsif ENV['HEROKU_ORGANIZATION'] && ENV['HEROKU_ORGANIZATION'].strip != "" ENV['HEROKU_ORGANIZATION'] elsif options[:ignore_no_org] nil From 2e7a7f370acf19a3568f4b683741fecc509dafa7 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Tue, 17 Feb 2015 13:17:32 -0600 Subject: [PATCH 323/952] Updated --buildpack option on create command to use new buildpack API --- lib/heroku/command/apps.rb | 4 ++-- spec/heroku/command/apps_spec.rb | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 9f13db498..d105ea45c 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -267,8 +267,8 @@ def create end if buildpack = options[:buildpack] - api.put_config_vars(info["name"], "BUILDPACK_URL" => buildpack) - display("BUILDPACK_URL=#{buildpack}") + api.put_app_buildpacks_v3(info['name'], {:updates => [{:buildpack => buildpack}]}) + display "Buildpack set. Next release on #{info['name']} will use #{buildpack}." end hputs([ info["web_url"], git_url(info['name']) ].join(" | ")) diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 250fa95a2..53d326908 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -136,12 +136,13 @@ module Heroku::Command end it "with a buildpack" do + Excon.stub({:method => :put, :path => "/apps/buildpackapp/buildpack-installations"}, {:status => 200}) with_blank_git_repository do stderr, stdout = execute("apps:create buildpackapp --buildpack http://example.org/buildpack.git") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Creating buildpackapp... done, stack is bamboo-mri-1.9.2 -BUILDPACK_URL=http://example.org/buildpack.git +Buildpack set. Next release on buildpackapp will use http://example.org/buildpack.git. http://buildpackapp.herokuapp.com/ | https://git.heroku.com/buildpackapp.git Git remote heroku added STDOUT From cfadf2270c72e281b0ab29b1f93fbbfaea8ffe8d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Feb 2015 11:06:24 -0800 Subject: [PATCH 324/952] v3.26.1 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6b57d9710..579b1ced6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.26.1 2015-02-18 +================= +Added buildpack command +Ignore HEROKU_ORGANIZATION env var if blank +Added OpenBSD to v4 plugins + 3.26.0 2015-02-10 ================= Removed default orgs in place of HEROKU_ORGANIZATION env var (#1395) diff --git a/Gemfile.lock b/Gemfile.lock index ab623e154..b84145cd3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.26.0) + heroku (3.26.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index cad234125..e3a48e75c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.26.0" + VERSION = "3.26.1" end From 3d8379cc063c108f0782bdcc3c8320211f6ed175 Mon Sep 17 00:00:00 2001 From: Troels Thomsen Date: Mon, 23 Feb 2015 12:53:58 +0100 Subject: [PATCH 325/952] Deprecate "heroku-push" plug-in --- lib/heroku/plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index 11778d770..e5fe7ce5c 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -18,6 +18,7 @@ class ErrorUpdatingSymlinkPlugin < StandardError; end heroku-netrc heroku-pgdumps heroku-postgresql + heroku-push heroku-releases heroku-shared-postgresql heroku-sql-console From fd06c89828229cc517ee5a2d343b0a9b8a35b6d9 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 23 Feb 2015 22:15:30 -0800 Subject: [PATCH 326/952] Make command help consistent with others for pg:copy --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 1edb1f90e..bcc35eb00 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -4,7 +4,7 @@ require "heroku/helpers/heroku_postgresql" class Heroku::Command::Pg < Heroku::Command::Base - # pg:copy source target + # pg:copy SOURCE TARGET # # Copy all data from source database to target. At least one of # these must be a Heroku Postgres database. From eeee99b9257c8bb01069ad4a49291f28541da67c Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 23 Feb 2015 22:19:17 -0800 Subject: [PATCH 327/952] Require confirmation for dangerous pg:backups commands --- lib/heroku/command/pg_backups.rb | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index bcc35eb00..76bcbb40f 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -23,9 +23,11 @@ def copy attachment = target.attachment || source.attachment - xfer = hpg_client(attachment).pg_copy(source.name, source.url, - target.name, target.url) - poll_transfer('copy', xfer[:uuid]) + if confirm_command + xfer = hpg_client(attachment).pg_copy(source.name, source.url, + target.name, target.url) + poll_transfer('copy', xfer[:uuid]) + end end # pg:backups [subcommand] @@ -311,8 +313,9 @@ def restore_backup abort("Backup #{backup_id} did not complete successfully; cannot restore it.") end - backup = hpg_client(attachment).backups_restore(backup[:to_url]) - display <<-EOF + if confirm_command + backup = hpg_client(attachment).backups_restore(backup[:to_url]) + display <<-EOF Use Ctrl-C at any time to stop monitoring progress; the backup will continue restoring. Use heroku pg:backups to check progress. Stop a running restore with heroku pg:backups cancel. @@ -320,7 +323,8 @@ def restore_backup #{transfer_name(backup[:num])} ---restore---> #{attachment.name} EOF - poll_transfer('restore', backup[:uuid]) + poll_transfer('restore', backup[:uuid]) + end end def poll_transfer(action, transfer_id) @@ -357,8 +361,10 @@ def delete_backup backup_id = shift_argument validate_arguments! - hpg_app_client(app).transfers_delete(backup_num(backup_id)) - display "Deleted #{backup_id}" + if confirm_command + hpg_app_client(app).transfers_delete(backup_num(backup_id)) + display "Deleted #{backup_id}" + end end def public_url From 257e5819fac6e296b4536411e42539b99fa90065 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 23 Feb 2015 22:06:00 -0800 Subject: [PATCH 328/952] Support scheduling backups for arbitrary config vars This is handy for, e.g., taking backups off a FOLLOWER_URL or an auxilliary database on the same app. --- lib/heroku/command/pg_backups.rb | 16 +++++++++++++++- lib/heroku/helpers/heroku_postgresql.rb | 10 +++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 76bcbb40f..631879549 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -391,7 +391,21 @@ def schedule_backups at = options[:at] || '04:00 UTC' schedule_opts = parse_schedule_time(at) - attachment = generate_resolver.resolve(db, "DATABASE_URL") + resolver = generate_resolver + attachment = resolver.resolve(db, "DATABASE_URL") + + # N.B.: we need to resolve the name to find the right database, + # but we don't want to resolve it to the canonical name, so that, + # e.g., names like FOLLOWER_URL work. To do this, we look up the + # app config vars and re-find one that looks like the user's + # requested name. + db_name, alias_url = resolver.app_config_vars.find { |k,_| k =~ /#{db}/i } + if attachment.url != alias_url + error("Could not find database to schedule for backups. Try using its full name.") + end + + schedule_opts[:schedule_name] = db_name + hpg_client(attachment).schedule(schedule_opts) display "Scheduled automatic daily backups at #{at} for #{attachment.name}" end diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index a8103d471..8cd1b1fd9 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -80,6 +80,11 @@ def hpg_addon_name end end + def app_config_vars + protect_missing_app + @app_config_vars ||= api.get_config_vars(app_name).body + end + private def protect_missing_app @@ -89,11 +94,6 @@ def protect_missing_app end end - def app_config_vars - protect_missing_app - @app_config_vars ||= api.get_config_vars(app_name).body - end - def app_attachments protect_missing_app @app_attachments ||= api.get_attachments(app_name).body.map { |raw| Attachment.new(raw) } From 8fee43357e9ad2526f4f62b0adc29a411c476e99 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 24 Feb 2015 16:36:03 -0800 Subject: [PATCH 329/952] upgraded netrc to 0.10.3 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b84145cd3..212bd734a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -38,7 +38,7 @@ GEM addressable (~> 2.3) mime-types (1.25.1) multi_json (1.10.1) - netrc (0.10.2) + netrc (0.10.3) rake (10.4.2) rest-client (1.6.7) mime-types (>= 1.16) From 26ee0415638dfd9248ec3eaf37cab40e29dab161 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 24 Feb 2015 16:51:47 -0800 Subject: [PATCH 330/952] Fix restore labeling --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 631879549..5d36d3045 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -171,7 +171,7 @@ def list_backups "created_at" => r[:created_at], "status" => transfer_status(r), "size" => size_pretty(r[:processed_bytes]), - "database" => r[:from_name] || 'UNKNOWN' + "database" => r[:to_name] || 'UNKNOWN' } end if display_restores.empty? From b4c63e7199eaa8f0c3d6e1bc08bda9c6ca326a23 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 24 Feb 2015 17:03:49 -0800 Subject: [PATCH 331/952] v3.27.0 --- CHANGELOG | 8 ++++++++ lib/heroku/version.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 579b1ced6..75b9d7d04 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.27.0 2015-02-24 +================= +Make pgbackups work with config vars other than DATABASE_URL +Require confirmation for dangerous pg:backups commands +Updated netrc to 0.10.3 +Fix pg:backups unschedule +Deprecated heroku-push plugin + 3.26.1 2015-02-18 ================= Added buildpack command diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e3a48e75c..1c8d67af3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.26.1" + VERSION = "3.27.0" end From 2ac1b347b3e36ab6cdaa54fa0958edc011181a19 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 24 Feb 2015 17:07:00 -0800 Subject: [PATCH 332/952] v3.27.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 212bd734a..add4e5fc3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.26.1) + heroku (3.27.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) From 99cf576965924a9754db261e43b124e449c5c13a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 24 Feb 2015 17:10:14 -0800 Subject: [PATCH 333/952] v3.27.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 75b9d7d04..3877ba6bd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.27.1 2015-02-24 +================= +Bumped gem version number to 3.27.1 + 3.27.0 2015-02-24 ================= Make pgbackups work with config vars other than DATABASE_URL diff --git a/Gemfile.lock b/Gemfile.lock index add4e5fc3..304374c15 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.27.0) + heroku (3.27.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1c8d67af3..e5fa41e74 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.27.0" + VERSION = "3.27.1" end From 1046367ef884da49ec3d6f8083e936be01d36322 Mon Sep 17 00:00:00 2001 From: David Sanders Date: Fri, 27 Feb 2015 02:58:13 +1100 Subject: [PATCH 334/952] Added 'restart' to usage message Added 'restart' to list of primary commands in usage message --- lib/heroku/command/help.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/help.rb b/lib/heroku/command/help.rb index 2fa8449e2..89e8b14cc 100644 --- a/lib/heroku/command/help.rb +++ b/lib/heroku/command/help.rb @@ -5,7 +5,7 @@ # class Heroku::Command::Help < Heroku::Command::Base - PRIMARY_NAMESPACES = %w( auth apps ps run addons config releases domains logs sharing ) + PRIMARY_NAMESPACES = %w( auth apps ps run restart addons config releases domains logs sharing ) include Heroku::Deprecated::Help From facb44cfd430e378f5acf44fa4e57b3d4ca0d842 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 26 Feb 2015 15:16:59 -0800 Subject: [PATCH 335/952] hide default orgs command --- lib/heroku/command/orgs.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/heroku/command/orgs.rb b/lib/heroku/command/orgs.rb index 5ed5d9f03..9b585d7b8 100644 --- a/lib/heroku/command/orgs.rb +++ b/lib/heroku/command/orgs.rb @@ -45,9 +45,7 @@ def open launchy("Opening web interface for #{org}", "https://dashboard.heroku.com/orgs/#{org}/apps") end - # orgs:default - # - # DEPRECATED: Use HEROKU_ORGANIZATION environment variable + # HIDDEN: orgs:default # def default display("DEPRECATED: Use HEROKU_ORGANIZATION environment variable.") From b200f1c97fed92fe283986323070158b461ffaef Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 26 Feb 2015 15:20:58 -0800 Subject: [PATCH 336/952] better error message for orgs:default --- lib/heroku/command/orgs.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/orgs.rb b/lib/heroku/command/orgs.rb index 9b585d7b8..8759a21e6 100644 --- a/lib/heroku/command/orgs.rb +++ b/lib/heroku/command/orgs.rb @@ -48,7 +48,7 @@ def open # HIDDEN: orgs:default # def default - display("DEPRECATED: Use HEROKU_ORGANIZATION environment variable.") + error("orgs:default is no longer in the CLI.\nUse the HEROKU_ORGANIZATION environment variable instead.\nSee https://devcenter.heroku.com/articles/develop-orgs#default-org for more info.") end end From b3669c9c16adcd5aae2cc8f2443d274701143152 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 26 Feb 2015 16:58:07 -0800 Subject: [PATCH 337/952] Allow restores from a URL --- lib/heroku/command/pg_backups.rb | 48 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 5d36d3045..68d4b6f8a 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -283,47 +283,55 @@ def capture_backup def restore_backup # heroku pg:backups restore [[backup_id] database] db = nil - backup_id = :latest + restore_from = :latest # N.B.: we have to account for the command argument here if args.count == 2 db = shift_argument elsif args.count == 3 - backup_id = shift_argument + restore_from = shift_argument db = shift_argument end attachment = generate_resolver.resolve(db, "DATABASE_URL") validate_arguments! - backups = hpg_app_client(app).transfers.select do |b| - b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' - end - backup = if backup_id == :latest - # N.B.: this also handles the empty backups case - backups.sort_by { |b| b[:started_at] }.last - else - backups.find { |b| transfer_name(b[:num]) == backup_id } - end - if backups.empty? - abort("No backups. Capture one with `heroku pg:backups capture`.") - elsif backup.nil? - abort("Backup #{backup_id} not found.") - elsif !backup[:succeeded] - abort("Backup #{backup_id} did not complete successfully; cannot restore it.") + restore_url = nil + if restore_from =~ %r{\Ahttps?://} + restore_url = restore_from + else + # assume we're restoring from a backup + backup_id = restore_from + backups = hpg_app_client(app).transfers.select do |b| + b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' + end + backup = if backup_id == :latest + # N.B.: this also handles the empty backups case + backups.sort_by { |b| b[:started_at] }.last + else + backups.find { |b| transfer_name(b[:num]) == backup_id } + end + if backups.empty? + abort("No backups. Capture one with `heroku pg:backups capture`.") + elsif backup.nil? + abort("Backup #{backup_id} not found.") + elsif !backup[:succeeded] + abort("Backup #{backup_id} did not complete successfully; cannot restore it.") + end + restore_url = backup[:to_url] end if confirm_command - backup = hpg_client(attachment).backups_restore(backup[:to_url]) + restore = hpg_client(attachment).backups_restore(restore_url) display <<-EOF Use Ctrl-C at any time to stop monitoring progress; the backup will continue restoring. Use heroku pg:backups to check progress. Stop a running restore with heroku pg:backups cancel. -#{transfer_name(backup[:num])} ---restore---> #{attachment.name} +#{transfer_name(restore[:num])} ---restore---> #{attachment.name} EOF - poll_transfer('restore', backup[:uuid]) + poll_transfer('restore', restore[:uuid]) end end From ada17dfa5393e3579866f41dbec26d5d8b7e7018 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 26 Feb 2015 17:34:31 -0800 Subject: [PATCH 338/952] pass all args to jsplugins --- lib/heroku/jsplugin.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 05550f7de..b8e73dc0e 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -22,8 +22,7 @@ def initialize(args, opts) end end klass.send(:define_method, :run) do - ENV['HEROKU_APP'] = @opts[:app] - exec this.bin, "#{plugin['topic']}:#{plugin['command']}", *@args + exec this.bin, "#{plugin['topic']}:#{plugin['command']}", *ARGV[1..-1] end Heroku::Command.register_command( :command => plugin['command'] ? "#{plugin['topic']}:#{plugin['command']}" : plugin['topic'], From 97803322bfee6659d500b6312623a3cef0e5cce1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 26 Feb 2015 18:04:51 -0800 Subject: [PATCH 339/952] switch to fullHelp for jsplugins --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index b8e73dc0e..71e6ce03f 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -31,7 +31,7 @@ def initialize(args, opts) :method => :run, :banner => plugin['usage'], :summary => plugin['description'], - :help => "\n#{plugin['help']}" + :help => "\n#{plugin['fullHelp']}" ) end end From cd1cf68ff41dca75071fae1d74caf6138525ca80 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 26 Feb 2015 18:14:55 -0800 Subject: [PATCH 340/952] v3.27.2 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3877ba6bd..fb47e860d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.27.2 2015-02-24 +================= +Added restart to primary commands in help +Fixed issue with argument passing to v4 plugins +Added full help for v4 plugins + 3.27.1 2015-02-24 ================= Bumped gem version number to 3.27.1 diff --git a/Gemfile.lock b/Gemfile.lock index 304374c15..6623019ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.27.1) + heroku (3.27.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e5fa41e74..d613e352c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.27.1" + VERSION = "3.27.2" end From 2e8298a1b4886033cb2341536636cfc1d208eef2 Mon Sep 17 00:00:00 2001 From: Naaman Newbold Date: Fri, 27 Feb 2015 10:36:26 -0800 Subject: [PATCH 341/952] Always Allow Org Opts Orgs are no longer a separate service and should work in alternate clouds. This removes the `skip_org?` check in the command base. --- lib/heroku/command/base.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 5e5e84d09..07586ad95 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -41,9 +41,7 @@ def org @nil = false options[:ignore_no_app] = true - @org ||= if skip_org? - nil - elsif options[:org].is_a?(String) + @org ||= if options[:org].is_a?(String) options[:org] elsif options[:personal] || @nil nil @@ -246,13 +244,7 @@ def org_from_app! options[:org] = extract_org_from_app options[:personal] = true unless options[:org] end - - def skip_org? - return false if ENV['HEROKU_CLOUD'].nil? - - !%w{default production prod}.include? ENV['HEROKU_CLOUD'] - end - + def git_url(app_name) if options[:ssh_git] "git@#{Heroku::Auth.git_host}:#{app_name}.git" From b7e0e43fce1214da98090d2f7f603db423ffcaa6 Mon Sep 17 00:00:00 2001 From: Naaman Newbold Date: Fri, 27 Feb 2015 10:46:05 -0800 Subject: [PATCH 342/952] Whitespace --- lib/heroku/command/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 07586ad95..534415fda 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -244,7 +244,7 @@ def org_from_app! options[:org] = extract_org_from_app options[:personal] = true unless options[:org] end - + def git_url(app_name) if options[:ssh_git] "git@#{Heroku::Auth.git_host}:#{app_name}.git" From 79a894a1c16ea8f3c1b5d96dc6ee9ccf692a9f30 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 15:37:50 -0800 Subject: [PATCH 343/952] cleaner help for jsplugins --- lib/heroku/jsplugin.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 71e6ce03f..37fb0ccbf 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -15,6 +15,7 @@ def self.load! ) unless Heroku::Command.namespaces.include?(topic['name']) end commands.each do |plugin| + help = "\n\n #{plugin['fullHelp'].split("\n").join("\n ")}" klass = Class.new do def initialize(args, opts) @args = args @@ -31,7 +32,7 @@ def initialize(args, opts) :method => :run, :banner => plugin['usage'], :summary => plugin['description'], - :help => "\n#{plugin['fullHelp']}" + :help => help ) end end From d98bd564e35e708c714a28403a63c4bb39f576d7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 16:21:54 -0800 Subject: [PATCH 344/952] added option to customize pg:wait interval --- lib/heroku/command/pg.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 5a97052a3..42cd0cea4 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -183,16 +183,19 @@ def unfollow # # defaults to all databases if no DATABASE is specified # + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) + # def wait requires_preauth db = shift_argument validate_arguments! + interval = options[:wait_interval] if db - wait_for generate_resolver.resolve(db) + wait_for(generate_resolver.resolve(db), interval) else generate_resolver.all_databases.values.each do |attach| - wait_for(attach) + wait_for(attach, interval) end end end @@ -538,17 +541,18 @@ def hpg_info_display(item) end end - def ticking + def ticking(interval) + interval = 1 unless interval ticks = 0 loop do yield(ticks) ticks +=1 - sleep 1 + sleep interval end end - def wait_for(attach) - ticking do |ticks| + def wait_for(attach, interval) + ticking(interval) do |ticks| status = hpg_client(attach).get_wait_status error status[:message] if status[:error?] break if !status[:waiting?] && ticks.zero? From 44ce0273b39756967da1d21b6cf62cc7ed16e7c9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 16:03:25 -0800 Subject: [PATCH 345/952] heroku local --- lib/heroku/command/local.rb | 32 ++++++++++++++++++++++++++++++++ lib/heroku/jsplugin.rb | 11 ++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 lib/heroku/command/local.rb diff --git a/lib/heroku/command/local.rb b/lib/heroku/command/local.rb new file mode 100644 index 000000000..cd3129beb --- /dev/null +++ b/lib/heroku/command/local.rb @@ -0,0 +1,32 @@ +require "heroku/command/base" + +module Heroku::Command + + # run heroku app locally + class Local < Base + + # local:start [PROCESSNAME] + # + # run heroku app locally + # + # Start the application specified by a Procfile (defaults to ./Procfile) + # + # Examples: + # + # heroku local:start + # heroku local:start web + # heroku local:start -f Procfile.test -e .env.test + # + # -f, --procfile PROCFILE + # -e, --env ENV + # -c, --concurrency CONCURRENCY + # -p, --port PORT + # -r, --r + # + def start + Heroku::JSPlugin.setup + Heroku::JSPlugin.install('heroku-local') unless Heroku::JSPlugin.is_plugin_installed?('heroku-local') + Heroku::JSPlugin.run('local', 'start', ARGV[1..-1]) + end + end +end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 37fb0ccbf..6166dde28 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -23,7 +23,7 @@ def initialize(args, opts) end end klass.send(:define_method, :run) do - exec this.bin, "#{plugin['topic']}:#{plugin['command']}", *ARGV[1..-1] + this.run(plugin['topic'], plugin['command'], ARGV[1..-1]) end Heroku::Command.register_command( :command => plugin['command'] ? "#{plugin['topic']}:#{plugin['command']}" : plugin['topic'], @@ -45,6 +45,10 @@ def self.plugins end end + def self.is_plugin_installed?(name) + plugins.any? { |p| p[:name] == name } + end + def self.topics commands_info['topics'] rescue @@ -97,6 +101,11 @@ def self.setup end end + def self.run(topic, command, args) + cmd = command ? "#{topic}:#{command}" : topic + exec self.bin, cmd, *args + end + def self.arch case RUBY_PLATFORM when /i386/ From 67d97716600c73256fef36499250d59245a5d4ef Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 16:07:44 -0800 Subject: [PATCH 346/952] push description out one space for jsplugins --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 6166dde28..c20424ab3 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -31,7 +31,7 @@ def initialize(args, opts) :klass => klass, :method => :run, :banner => plugin['usage'], - :summary => plugin['description'], + :summary => " #{plugin['description']}", :help => help ) end From 502b0f0a758960fbb7a707faff1a941f3a348567 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 16:29:30 -0800 Subject: [PATCH 347/952] v3.28.0 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index fb47e860d..7594303b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.28.0 2015-02-27 +================= +Added `heroku local` +Added flag to customize poll interval for pg:wait +Fixed error message for default orgs +Allow org for all commands +Improved help formatting for v4 plugins + 3.27.2 2015-02-24 ================= Added restart to primary commands in help diff --git a/Gemfile.lock b/Gemfile.lock index 6623019ab..fee23e01e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.27.2) + heroku (3.28.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d613e352c..bd81e2e4f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.27.2" + VERSION = "3.28.0" end From a2120ca530819d3275d60b29ab836eb6f0eb18fe Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 16:39:49 -0800 Subject: [PATCH 348/952] renamed local:start to just local --- lib/heroku/command/local.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/heroku/command/local.rb b/lib/heroku/command/local.rb index cd3129beb..b6a195a12 100644 --- a/lib/heroku/command/local.rb +++ b/lib/heroku/command/local.rb @@ -5,7 +5,7 @@ module Heroku::Command # run heroku app locally class Local < Base - # local:start [PROCESSNAME] + # local [PROCESSNAME] # # run heroku app locally # @@ -13,9 +13,9 @@ class Local < Base # # Examples: # - # heroku local:start - # heroku local:start web - # heroku local:start -f Procfile.test -e .env.test + # heroku local + # heroku local web + # heroku local -f Procfile.test -e .env.test # # -f, --procfile PROCFILE # -e, --env ENV @@ -23,10 +23,10 @@ class Local < Base # -p, --port PORT # -r, --r # - def start + def index Heroku::JSPlugin.setup Heroku::JSPlugin.install('heroku-local') unless Heroku::JSPlugin.is_plugin_installed?('heroku-local') - Heroku::JSPlugin.run('local', 'start', ARGV[1..-1]) + Heroku::JSPlugin.run('local', nil, ARGV[1..-1]) end end end From 115c36e6a1d8be94857fd8f9e8e87364f305fa8d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 27 Feb 2015 16:40:39 -0800 Subject: [PATCH 349/952] v3.28.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7594303b1..3dcbf1855 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.28.1 2015-02-27 +================= +Renamed local:start to just local + 3.28.0 2015-02-27 ================= Added `heroku local` diff --git a/Gemfile.lock b/Gemfile.lock index fee23e01e..0defd4935 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.0) + heroku (3.28.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index bd81e2e4f..206b93eeb 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.0" + VERSION = "3.28.1" end From fcb52987e5dcc41f25f47e18da9c513a641a3cb0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 2 Mar 2015 13:47:26 -0800 Subject: [PATCH 350/952] fix wait interval for pg:wait --- lib/heroku/command/pg.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 42cd0cea4..67d6332fd 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -189,7 +189,8 @@ def wait requires_preauth db = shift_argument validate_arguments! - interval = options[:wait_interval] + interval = options[:wait_interval].to_i + interval = 1 if interval < 1 if db wait_for(generate_resolver.resolve(db), interval) @@ -542,7 +543,6 @@ def hpg_info_display(item) end def ticking(interval) - interval = 1 unless interval ticks = 0 loop do yield(ticks) From f916c90a40d44464638e7560fd7bfb0dbd563e01 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 2 Mar 2015 13:50:31 -0800 Subject: [PATCH 351/952] v3.28.2 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3dcbf1855..be8d72340 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.28.2 2015-03-02 +================= +Fixed bug with --wait-interval flag for pg:wait + 3.28.1 2015-02-27 ================= Renamed local:start to just local diff --git a/Gemfile.lock b/Gemfile.lock index 0defd4935..cb3cfee30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.1) + heroku (3.28.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 206b93eeb..3f76e7789 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.1" + VERSION = "3.28.2" end From 03441be8ebfb4d555fe621c571b4d4fc9e1d6838 Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Tue, 3 Mar 2015 14:59:58 -0800 Subject: [PATCH 352/952] use the new pgbackups language --- lib/heroku/command/fork.rb | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 6b0855619..7c43b2e83 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -76,8 +76,6 @@ def index wait_for_db to, to_addon end - check_for_pgbackups! from - check_for_pgbackups! to migrate_db addon, from, to_addon, to end end @@ -127,14 +125,6 @@ def copy_slug(from_info, to_info) :deploy_source => from_info["id"]) end - def check_for_pgbackups!(app) - unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ } - action("Adding pgbackups:plus to #{app}") do - api.post_addon app, "pgbackups:plus" - end - end - end - def migrate_db(from_addon, from, to_addon, to) transfer = nil @@ -144,13 +134,19 @@ def migrate_db(from_addon, from, to_addon, to) to_config = api.get_config_vars(to).body to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - pgb = Heroku::Client::Pgbackups.new(from_config["PGBACKUPS_URL"]) - transfer = pgb.create_transfer( - from_config["#{from_attachment}_URL"], + attachment = Heroku::Helpers::HerokuPostgresql::Attachment.new( + 'app' => {'name' => from }, + 'name' => from_attachment, + 'config_var' => "#{from_attachment}_URL", + 'resource' => {'name' => from, + 'value' => from_addon['sso_url'], + 'type' => from_addon['name']}) + pgb = Heroku::Client::HerokuPostgresql.new(attachment) + transfer = pgb.pg_copy( from_attachment, - to_config["#{to_attachment}_URL"], + from_config["#{from_attachment}_URL"], to_attachment, - :expire => "true") + to_config["#{to_attachment}_URL"]) error transfer["errors"].values.flatten.join("\n") if transfer["errors"] loop do From e1b5632cb25b7f583b609be303f65f6674414b82 Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Wed, 4 Mar 2015 11:10:49 -0800 Subject: [PATCH 353/952] forgot to take care of the checks to see if the backup happened successfully --- lib/heroku/command/fork.rb | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 7c43b2e83..69cde9a65 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -134,27 +134,21 @@ def migrate_db(from_addon, from, to_addon, to) to_config = api.get_config_vars(to).body to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - attachment = Heroku::Helpers::HerokuPostgresql::Attachment.new( - 'app' => {'name' => from }, - 'name' => from_attachment, - 'config_var' => "#{from_attachment}_URL", - 'resource' => {'name' => from, - 'value' => from_addon['sso_url'], - 'type' => from_addon['name']}) + resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new('pgbackups-rims', api) + attachment = resolver.resolve("#{from_attachment}_URL", nil) pgb = Heroku::Client::HerokuPostgresql.new(attachment) transfer = pgb.pg_copy( - from_attachment, + from_attachment.gsub('HEROKU_POSTGRESQL_',''), from_config["#{from_attachment}_URL"], - to_attachment, + to_attachment.gsub('HEROKU_POSTGRESQL_',''), to_config["#{to_attachment}_URL"]) error transfer["errors"].values.flatten.join("\n") if transfer["errors"] - loop do - transfer = pgb.get_transfer(transfer["id"]) + begin + transfer = pgb.backups_get(transfer[:uuid]) error transfer["errors"].values.flatten.join("\n") if transfer["errors"] - break if transfer["finished_at"] sleep 1 - end + end until transfer[:finished_at] print " " end end From 51dae224a41712a5116611128bb5cd4a6c917886 Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Wed, 4 Mar 2015 11:24:58 -0800 Subject: [PATCH 354/952] argh! --- lib/heroku/command/fork.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 69cde9a65..9dc7a07cd 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -134,7 +134,7 @@ def migrate_db(from_addon, from, to_addon, to) to_config = api.get_config_vars(to).body to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new('pgbackups-rims', api) + resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(from, api) attachment = resolver.resolve("#{from_attachment}_URL", nil) pgb = Heroku::Client::HerokuPostgresql.new(attachment) transfer = pgb.pg_copy( From 606d8fcd11669710bdf0331c4939527e5faf8fc4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 6 Mar 2015 16:50:52 -0800 Subject: [PATCH 355/952] show message that toolbelt v4 is installing --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index c20424ab3..78f364fc5 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -89,6 +89,7 @@ def self.bin def self.setup return if File.exist? bin + $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) resp = Excon.get(url, :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) open(bin, "wb") do |file| @@ -99,6 +100,7 @@ def self.setup File.delete bin raise 'SHA mismatch for heroku-cli' end + $stderr.puts " done" end def self.run(topic, command, args) From a26d3057e547fd92ea847878825e4b321fbc702c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 9 Mar 2015 22:37:00 -0700 Subject: [PATCH 356/952] v3.28.3 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index be8d72340..e987b8a04 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.28.3 2015-03-09 +================= +Show message that toolbelt v4 is installing +Changed fork to use new pgbackup implementation + 3.28.2 2015-03-02 ================= Fixed bug with --wait-interval flag for pg:wait diff --git a/Gemfile.lock b/Gemfile.lock index cb3cfee30..6a4b68e4a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.2) + heroku (3.28.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3f76e7789..43cad0987 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.2" + VERSION = "3.28.3" end From fa5d5cda09a968db329a73961c6484510866750e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 9 Mar 2015 23:58:00 -0700 Subject: [PATCH 357/952] v3.28.4 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6a4b68e4a..58e7f7c98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.3) + heroku (3.28.4) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 43cad0987..1caf4a773 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.3" + VERSION = "3.28.4" end From 57a9509f4e74f2125918e30e1f250b098fa9e3f4 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 10 Mar 2015 09:14:43 -0700 Subject: [PATCH 358/952] Revert "use the new postgres backups" --- lib/heroku/command/fork.rb | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 9dc7a07cd..6b0855619 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -76,6 +76,8 @@ def index wait_for_db to, to_addon end + check_for_pgbackups! from + check_for_pgbackups! to migrate_db addon, from, to_addon, to end end @@ -125,6 +127,14 @@ def copy_slug(from_info, to_info) :deploy_source => from_info["id"]) end + def check_for_pgbackups!(app) + unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ } + action("Adding pgbackups:plus to #{app}") do + api.post_addon app, "pgbackups:plus" + end + end + end + def migrate_db(from_addon, from, to_addon, to) transfer = nil @@ -134,21 +144,21 @@ def migrate_db(from_addon, from, to_addon, to) to_config = api.get_config_vars(to).body to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(from, api) - attachment = resolver.resolve("#{from_attachment}_URL", nil) - pgb = Heroku::Client::HerokuPostgresql.new(attachment) - transfer = pgb.pg_copy( - from_attachment.gsub('HEROKU_POSTGRESQL_',''), + pgb = Heroku::Client::Pgbackups.new(from_config["PGBACKUPS_URL"]) + transfer = pgb.create_transfer( from_config["#{from_attachment}_URL"], - to_attachment.gsub('HEROKU_POSTGRESQL_',''), - to_config["#{to_attachment}_URL"]) + from_attachment, + to_config["#{to_attachment}_URL"], + to_attachment, + :expire => "true") error transfer["errors"].values.flatten.join("\n") if transfer["errors"] - begin - transfer = pgb.backups_get(transfer[:uuid]) + loop do + transfer = pgb.get_transfer(transfer["id"]) error transfer["errors"].values.flatten.join("\n") if transfer["errors"] + break if transfer["finished_at"] sleep 1 - end until transfer[:finished_at] + end print " " end end From 4e52229ba0d2f4cf742a3df469bfc88b9a4a3881 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 10 Mar 2015 09:18:02 -0700 Subject: [PATCH 359/952] v3.28.5 --- CHANGELOG | 4 ++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e987b8a04..1cc55e5e8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.28.4 2015-03-10 +================= +Reverted new pgbackup implementation for fork + 3.28.3 2015-03-09 ================= Show message that toolbelt v4 is installing diff --git a/Gemfile.lock b/Gemfile.lock index 58e7f7c98..73698037a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.4) + heroku (3.28.5) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.43.0) + excon (0.44.4) fakefs (0.5.4) heroku-api (0.3.22) excon (~> 0.38) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1caf4a773..4814320e1 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.4" + VERSION = "3.28.5" end From 853a55a7cedf07be3b3424266be1988308a3fd78 Mon Sep 17 00:00:00 2001 From: Dan Peterson Date: Tue, 10 Mar 2015 11:31:28 -0300 Subject: [PATCH 360/952] full_host doesn't require an arg, use full_host_uri.host for checking netrc. --- lib/heroku/auth.rb | 18 +++++++++++++----- lib/heroku/client/organizations.rb | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index ffce59e1a..9c1e795ee 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -166,15 +166,17 @@ def read_credentials # read netrc credentials if they exist if netrc + netrc_host = full_host_uri.host + # force migration of long api tokens (80 chars) to short ones (40) # #write_credentials rewrites both api.* and code.* - credentials = netrc["api.#{host}"] + credentials = netrc[netrc_host] if credentials && credentials[1].length > 40 @credentials = [ credentials[0], credentials[1][0,40] ] write_credentials end - netrc["api.#{host}"] + netrc[netrc_host] end end end @@ -349,8 +351,14 @@ def base_host(host) parts[-2..-1].join(".") end - def full_host(host) - (host =~ /^http/) ? host : "https://api.#{host}" + def full_host(*args) + # backwards compat for when this took an arg + h = args.first || host + (h =~ /^http/) ? h : "https://api.#{h}" + end + + def full_host_uri + URI.parse(full_host) end def verify_host?(host) @@ -361,7 +369,7 @@ def verify_host?(host) protected def default_params - uri = URI.parse(full_host(host)) + uri = full_host_uri params = { :headers => {'User-Agent' => Heroku.user_agent}, :host => uri.host, diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 31d21fc6f..8048a4082 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -212,7 +212,7 @@ def decompress_response!(response) end def manager_url - Heroku::Auth.full_host(Heroku::Auth.host) + Heroku::Auth.full_host end end From 520cbd52b66587666174c49cd5ba6d1c6729b421 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Mon, 2 Mar 2015 17:57:58 -0800 Subject: [PATCH 361/952] Initial sketch for presenting cedar as cedar-10 We discussed various ways to do this, and it seems the least obtrusive way is to do this at the boundaries (i.e. dashboard / cli) as opposed to changing internals. --- lib/heroku/command/apps.rb | 3 ++- lib/heroku/command/stack.rb | 30 ++++++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index d105ea45c..bac00686e 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -1,4 +1,5 @@ require "heroku/command/base" +require "heroku/command/stack" # manage apps (create, destroy) # @@ -255,7 +256,7 @@ def create status("region is #{region_from_app(info)}") else stack = (info['stack'].is_a?(Hash) ? info['stack']["name"] : info['stack']) - status("stack is #{stack}") + status("stack is #{Heroku::Command::Stack::Codex.out(stack)}") end end diff --git a/lib/heroku/command/stack.rb b/lib/heroku/command/stack.rb index 009177654..454094c95 100644 --- a/lib/heroku/command/stack.rb +++ b/lib/heroku/command/stack.rb @@ -23,7 +23,7 @@ def index styled_header("#{app} Available Stacks") stacks = stacks_data.map do |stack| - row = [stack['current'] ? '*' : ' ', stack['name']] + row = [stack['current'] ? '*' : ' ', Codex.out(stack['name'])] row << '(beta)' if stack['beta'] row << '(deprecated)' if stack['deprecated'] row << '(prepared, will migrate on next git push)' if stack['requested'] @@ -37,15 +37,37 @@ def index # set new app stack # def set - unless stack = shift_argument + unless stack = Codex.in(shift_argument) error("Usage: heroku stack:set STACK.\nMust specify target stack.") end api.put_stack(app, stack) - display "Stack set. Next release on #{app} will use #{stack}." - display "Run `git push heroku master` to create a new release on #{stack}." + display "Stack set. Next release on #{app} will use #{Codex.out(stack)}." + display "Run `git push heroku master` to create a new release on #{Codex.out(stack)}." end alias_command "stack:migrate", "stack:set" + + module Codex + def self.in(stack) + IN[stack] || stack + end + + def self.out(stack) + OUT[stack] || stack + end + + # Legacy translations for cedar => cedar-10 + # only here for UX purposes to avoid confusion + # when we say `Sunsetting cedar`. + IN = { + "cedar-10" => "cedar", + "cedar" => "cedar" + } + + OUT = { + "cedar" => "cedar-10" + } + end end end From f66af08c91391e94c28e0ec1ddbf16794efe86ee Mon Sep 17 00:00:00 2001 From: Cyril David Date: Tue, 10 Mar 2015 13:31:14 -0700 Subject: [PATCH 362/952] Remove unnecessary translation --- lib/heroku/command/stack.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/command/stack.rb b/lib/heroku/command/stack.rb index 454094c95..25f7bbda5 100644 --- a/lib/heroku/command/stack.rb +++ b/lib/heroku/command/stack.rb @@ -61,8 +61,7 @@ def self.out(stack) # only here for UX purposes to avoid confusion # when we say `Sunsetting cedar`. IN = { - "cedar-10" => "cedar", - "cedar" => "cedar" + "cedar-10" => "cedar" } OUT = { From 4e2b23e603fbe889147e8721aa7b4e299780d5fc Mon Sep 17 00:00:00 2001 From: Cyril David Date: Tue, 10 Mar 2015 13:39:36 -0700 Subject: [PATCH 363/952] Update spec to match the new behavior of stacks --- spec/heroku/command/stack_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/heroku/command/stack_spec.rb b/spec/heroku/command/stack_spec.rb index c0cc016ac..49bc5cea3 100644 --- a/spec/heroku/command/stack_spec.rb +++ b/spec/heroku/command/stack_spec.rb @@ -20,7 +20,7 @@ module Heroku::Command === example Available Stacks aspen-mri-1.8.6 bamboo-ree-1.8.7 - cedar (beta) + cedar-10 (beta) * bamboo-mri-1.9.2 STDOUT From ed80977ff667277de6c4675442dd9d529e758c10 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Tue, 10 Mar 2015 14:06:47 -0700 Subject: [PATCH 364/952] Apply the codex translation to apps:info --- lib/heroku/command/apps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index bac00686e..c9c9af3b6 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -174,7 +174,7 @@ def info data["Slug Size"] = format_bytes(app_data["slug_size"]) if app_data["slug_size"] data["Cache Size"] = format_bytes(app_data["cache_size"]) if app_data["cache_size"] - data["Stack"] = app_data["stack"] + data["Stack"] = Heroku::Command::Stack::Codex.out(app_data["stack"]) if data["Stack"] != "cedar" data.merge!("Dynos" => app_data["dynos"], "Workers" => app_data["workers"]) end From ba804550725720785d6d60ece46e100c40b0f2f5 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Tue, 10 Mar 2015 14:11:05 -0700 Subject: [PATCH 365/952] Fix specs --- lib/heroku/command/apps.rb | 2 +- spec/heroku/command/apps_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index c9c9af3b6..d926ba1b3 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -175,7 +175,7 @@ def info data["Cache Size"] = format_bytes(app_data["cache_size"]) if app_data["cache_size"] data["Stack"] = Heroku::Command::Stack::Codex.out(app_data["stack"]) - if data["Stack"] != "cedar" + if data["Stack"] != "cedar-10" data.merge!("Dynos" => app_data["dynos"], "Workers" => app_data["workers"]) end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index c3aea72ca..8253012df 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -26,7 +26,7 @@ module Heroku::Command === example Git URL: https://git.heroku.com/example.git Owner Email: email@example.com -Stack: cedar +Stack: cedar-10 Web URL: http://example.herokuapp.com/ STDOUT end @@ -38,7 +38,7 @@ module Heroku::Command === example Git URL: https://git.heroku.com/example.git Owner Email: email@example.com -Stack: cedar +Stack: cedar-10 Web URL: http://example.herokuapp.com/ STDOUT end From cafedaf3ce40b604b8bb93d366f0b97649fedf5b Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Tue, 10 Mar 2015 18:02:50 -0700 Subject: [PATCH 366/952] say who is using psql and how --- lib/heroku/command/pg.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 67d6332fd..bf5f0e9ac 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -106,6 +106,7 @@ def psql begin ENV["PGPASSWORD"] = uri.password ENV["PGSSLMODE"] = 'require' + ENV["PGAPPNAME"] = "#{pgappname} interactive" if command = options[:command] command = %Q(-c "#{command}") end @@ -615,6 +616,7 @@ def exec_sql_on_uri(sql,uri) begin ENV["PGPASSWORD"] = uri.password ENV["PGSSLMODE"] = (uri.host == 'localhost' ? 'prefer' : 'require' ) + ENV["PGAPPNAME"] = "#{pgappname} non-interactive" user_part = uri.user ? "-U #{uri.user}" : "" output = `#{psql_cmd} -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` if (! $?.success?) || output.nil? || output.empty? @@ -628,6 +630,14 @@ def exec_sql_on_uri(sql,uri) end end + def pgappname + if running_on_windows? + 'psql (windows)' + else + "psql #{`whoami`.chomp.gsub(/\W/,'')}" + end + end + def psql_cmd # some people alais psql, so we need to find the real psql # but windows doesn't have the command command From b2d57868219040f39344ee5e8f6c1e85b1fc28ac Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Tue, 10 Mar 2015 20:16:19 -0700 Subject: [PATCH 367/952] needed to use a different transfer command --- lib/heroku/command/fork.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 9dc7a07cd..efe1a0155 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -134,8 +134,8 @@ def migrate_db(from_addon, from, to_addon, to) to_config = api.get_config_vars(to).body to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(from, api) - attachment = resolver.resolve("#{from_attachment}_URL", nil) + resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(to, api) + attachment = resolver.resolve("#{to_attachment}_URL", nil) pgb = Heroku::Client::HerokuPostgresql.new(attachment) transfer = pgb.pg_copy( from_attachment.gsub('HEROKU_POSTGRESQL_',''), @@ -143,12 +143,14 @@ def migrate_db(from_addon, from, to_addon, to) to_attachment.gsub('HEROKU_POSTGRESQL_',''), to_config["#{to_attachment}_URL"]) - error transfer["errors"].values.flatten.join("\n") if transfer["errors"] + hpg_app_client = Heroku::Client::HerokuPostgresqlApp.new(to) begin - transfer = pgb.backups_get(transfer[:uuid]) - error transfer["errors"].values.flatten.join("\n") if transfer["errors"] + transfer = hpg_app_client.transfers_get(transfer[:uuid]) sleep 1 end until transfer[:finished_at] + if !transfer[:succeeded] + error "An error occurred and your transfer did not finish." + end print " " end end From e4e99694e22dcc95269d9e6b681abfb794d37601 Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Tue, 10 Mar 2015 20:25:49 -0700 Subject: [PATCH 368/952] bad merge --- lib/heroku/command/fork.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 62aede785..4a849c945 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -150,10 +150,8 @@ def migrate_db(from_addon, from, to_addon, to) transfer = pgb.pg_copy( from_attachment.gsub('HEROKU_POSTGRESQL_',''), from_config["#{from_attachment}_URL"], - from_attachment, - to_config["#{to_attachment}_URL"], - to_attachment, - :expire => "true") + to_attachment.gsub('HEROKU_POSTGRESQL_',''), + to_config["#{to_attachment}_URL"]) hpg_app_client = Heroku::Client::HerokuPostgresqlApp.new(to) begin From 99dd72e39e64929efcb9ee8581be9cc4fbec2765 Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Wed, 11 Mar 2015 14:14:19 -0700 Subject: [PATCH 369/952] remove the check for pgbackups since its now in pg namespace --- lib/heroku/command/fork.rb | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 4a849c945..efe1a0155 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -76,8 +76,6 @@ def index wait_for_db to, to_addon end - check_for_pgbackups! from - check_for_pgbackups! to migrate_db addon, from, to_addon, to end end @@ -127,14 +125,6 @@ def copy_slug(from_info, to_info) :deploy_source => from_info["id"]) end - def check_for_pgbackups!(app) - unless api.get_addons(app).body.detect { |addon| addon["name"] =~ /^pgbackups:/ } - action("Adding pgbackups:plus to #{app}") do - api.post_addon app, "pgbackups:plus" - end - end - end - def migrate_db(from_addon, from, to_addon, to) transfer = nil From 4acad390288426cd07b8a6bedeb0cb3bd2a75acf Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 11 Mar 2015 14:48:04 -0700 Subject: [PATCH 370/952] v3.28.6 --- CHANGELOG | 7 +++++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1cc55e5e8..7d7c8c6ea 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.28.6 2015-03-11 +================= +Changed fork to use new pgbackup implementation +Show who is using psql and how +Present cedar as cedar-10 +Use full_host_uri.host for checking netrc + 3.28.4 2015-03-10 ================= Reverted new pgbackup implementation for fork diff --git a/Gemfile.lock b/Gemfile.lock index 73698037a..ba0c2114f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.5) + heroku (3.28.6) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10.1) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.44.4) + excon (0.43.0) fakefs (0.5.4) heroku-api (0.3.22) excon (~> 0.38) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4814320e1..bd3f54a2c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.5" + VERSION = "3.28.6" end From de2a381426bae6b5c0f158f80596c8d2631abac3 Mon Sep 17 00:00:00 2001 From: Chris Gaffney Date: Thu, 12 Mar 2015 12:10:19 -0400 Subject: [PATCH 371/952] Relax the version requirement for MultiJson. Firstly I am trying to update the dependencies on a Rails app and bundler is attempting to downgrade the heroku gem due to an update for `multi_json`. Secondly it appears that the gem is using `MultiJson.dump` and `MultiJson.load` exclusively (`MultiJson.encode` is an alias to `dump`). These methods have been the public API since 1.6.0 with no discussion that I could find about changing them. Considering the surface area being used of MultiJson and that it's an incredibly common dependency it seems unnecessary to lock to to a specific micro version. Finally this replaces the one use of `MultiJson.encode` with `MultiJson.dump` which is the more common usage in the code. --- heroku.gemspec | 2 +- lib/heroku/command/two_factor.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/heroku.gemspec b/heroku.gemspec index 964869671..95134a271 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -25,5 +25,5 @@ Gem::Specification.new do |gem| gem.add_dependency "netrc", ">= 0.10.0" gem.add_dependency "rest-client", "= 1.6.7" gem.add_dependency "rubyzip", "= 0.9.9" - gem.add_dependency "multi_json", "~> 1.10.1" + gem.add_dependency "multi_json", "~> 1.10" end diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb index 84bcd2270..c9c94f56d 100644 --- a/lib/heroku/command/two_factor.rb +++ b/lib/heroku/command/two_factor.rb @@ -32,7 +32,7 @@ def disable print "Password (typing will be hidden): " password = Heroku::Auth.ask_for_password - update = MultiJson.encode( + update = MultiJson.dump( :two_factor_authentication => false, :password => password) From 9feae388382f2a4f259f0edc5e7781031d0ee0a9 Mon Sep 17 00:00:00 2001 From: Shirshendu Mukherjee Date: Tue, 17 Mar 2015 10:16:51 +0530 Subject: [PATCH 372/952] Add Request ID logging to STDERR --- lib/heroku/command.rb | 12 ++++++------ lib/heroku/helpers.rb | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 3d7bb00f4..c908c36dd 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -227,13 +227,13 @@ def self.run(cmd, arguments=[]) rescue Heroku::API::Errors::VerificationRequired, RestClient::PaymentRequired => e retry if Heroku::Helpers.confirm_billing rescue Heroku::API::Errors::NotFound => e - error extract_error(e.response.body) { + error(extract_error(e.response.body) { e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - } + }, nil, error: e) rescue RestClient::ResourceNotFound => e - error extract_error(e.http_body) { + error(extract_error(e.http_body) { e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - } + }, nil, error: e) rescue Heroku::API::Errors::Locked => e app = e.response.headers["X-Confirmation-Required"] if confirm_command(app, extract_error(e.response.body)) @@ -257,10 +257,10 @@ def self.run(cmd, arguments=[]) end retry else - error extract_error(e.response.body) + error extract_error(e.response.body), nil, error: e end rescue Heroku::API::Errors::ErrorWithResponse => e - error extract_error(e.response.body) + error extract_error(e.response.body), nil, error: e rescue RestClient::RequestFailed => e if e.response.code == 403 && e.response.headers.has_key?(:heroku_two_factor_required) Heroku::Auth.preauth diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 6051c86d0..f6ab05cda 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -274,7 +274,7 @@ def output_with_bang(message="", new_line=true) display(format_with_bang(message), new_line) end - def error(message, report=false) + def error(message, report=false, opts={}) if Heroku::Helpers.error_with_failure display("failed") Heroku::Helpers.error_with_failure = false @@ -282,6 +282,8 @@ def error(message, report=false) $stderr.puts(format_with_bang(message)) rollbar_id = Rollbar.error(message) if report $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id + request_id = opts[:error] && opts[:error].response ? opts[:error].response.headers['Request-Id'] : nil + $stderr.puts "Request ID: #{request_id}" if request_id exit(1) end From f07d657aae69f58ebe7e6b13166b12a09c57301b Mon Sep 17 00:00:00 2001 From: Shirshendu Mukherjee Date: Tue, 17 Mar 2015 11:32:17 +0530 Subject: [PATCH 373/952] Use ruby1.8 compatible hash syntax --- lib/heroku/command.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index c908c36dd..e4656e376 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -229,11 +229,11 @@ def self.run(cmd, arguments=[]) rescue Heroku::API::Errors::NotFound => e error(extract_error(e.response.body) { e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - }, nil, error: e) + }, nil, :error => e) rescue RestClient::ResourceNotFound => e error(extract_error(e.http_body) { e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - }, nil, error: e) + }, nil, :error => e) rescue Heroku::API::Errors::Locked => e app = e.response.headers["X-Confirmation-Required"] if confirm_command(app, extract_error(e.response.body)) @@ -257,10 +257,10 @@ def self.run(cmd, arguments=[]) end retry else - error extract_error(e.response.body), nil, error: e + error extract_error(e.response.body), nil, :error => e end rescue Heroku::API::Errors::ErrorWithResponse => e - error extract_error(e.response.body), nil, error: e + error extract_error(e.response.body), nil, :error => e rescue RestClient::RequestFailed => e if e.response.code == 403 && e.response.headers.has_key?(:heroku_two_factor_required) Heroku::Auth.preauth From 7903c553de57434162eb5285f76f3b4c2c57e4d3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 17 Mar 2015 12:38:20 -0700 Subject: [PATCH 374/952] fixed os checking for jruby --- lib/heroku/jsplugin.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 78f364fc5..30b490699 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -108,8 +108,12 @@ def self.run(topic, command, args) exec self.bin, cmd, *args end + def self.platform + RbConfig::CONFIG['host_os'] + end + def self.arch - case RUBY_PLATFORM + case platform when /i386/ "386" when /x64/ @@ -119,7 +123,7 @@ def self.arch end def self.os - case RUBY_PLATFORM + case platform when /darwin|mac os/ "darwin" when /linux/ @@ -129,7 +133,7 @@ def self.os when /openbsd/ "openbsd" else - raise "unsupported on #{RUBY_PLATFORM}" + raise "unsupported on #{platform}" end end From 4cffab305c06e262d0265868fd61944c6f81b2ae Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 17 Mar 2015 12:39:14 -0700 Subject: [PATCH 375/952] updated Gemfile.lock --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ba0c2114f..ba7e1dc8d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,7 +4,7 @@ PATH heroku (3.28.6) heroku-api (~> 0.3.19) launchy (>= 0.3.2) - multi_json (~> 1.10.1) + multi_json (~> 1.10) netrc (>= 0.10.0) rest-client (= 1.6.7) rubyzip (= 0.9.9) From 36dcf1ef8c1a4ff4a9e012c8d5d6b53c652e9bd2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 17 Mar 2015 16:25:01 -0700 Subject: [PATCH 376/952] removed heroku changelog step --- RELEASE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index de327008e..5cafc9e8b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,7 +11,6 @@ Releasing the CLI involves releasing a few different things. The important tasks * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality * Run `bundle install` to update the version of heroku in the `Gemfile.lock` * Update `CHANGELOG` -* Update Heroku Changelog (instructions below) * Commit the changes `git commit -m "vX.Y.Z" -a` * Push changes to master `git push origin master` * Go to the buildserver and release http://54.148.200.17/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) From 4dbe251b574a1c2ca2dda8b2e3aa40ebb99c86b0 Mon Sep 17 00:00:00 2001 From: Doug McInnes Date: Tue, 17 Mar 2015 16:44:25 -0700 Subject: [PATCH 377/952] fix os and arch detection --- lib/heroku/jsplugin.rb | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 30b490699..4cefbd2a0 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -108,22 +108,17 @@ def self.run(topic, command, args) exec self.bin, cmd, *args end - def self.platform - RbConfig::CONFIG['host_os'] - end - def self.arch - case platform - when /i386/ - "386" - when /x64/ - else + case RbConfig::CONFIG['host_cpu'] + when /x86_64/ "amd64" + else + "386" end end def self.os - case platform + case RbConfig::CONFIG['host_os'] when /darwin|mac os/ "darwin" when /linux/ @@ -133,7 +128,7 @@ def self.os when /openbsd/ "openbsd" else - raise "unsupported on #{platform}" + raise "unsupported on #{RbConfig::CONFIG['host_os']}" end end From e3d6fdfded2383b25387cc7fc8e041a5060c60fc Mon Sep 17 00:00:00 2001 From: Doug McInnes Date: Tue, 17 Mar 2015 16:50:16 -0700 Subject: [PATCH 378/952] v3.29.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7d7c8c6ea..c42f7b08e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.29.0 2015-03-17 +================= +Fixed architecture detection on jruby for jsplugins +Relaxed version requirement for multi_json +Added request ID logging on API errors + 3.28.6 2015-03-11 ================= Changed fork to use new pgbackup implementation diff --git a/Gemfile.lock b/Gemfile.lock index ba7e1dc8d..50938cfe7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.28.6) + heroku (3.29.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index bd3f54a2c..cea254df7 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.28.6" + VERSION = "3.29.0" end From e4c3d46287d288e75fe1821eef2cbb6fc950dc1e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 17 Mar 2015 17:07:44 -0700 Subject: [PATCH 379/952] use toolbelt v4 for fork --- lib/heroku/api/releases_v3.rb | 34 ------- lib/heroku/command/fork.rb | 167 +------------------------------ spec/heroku/command/fork_spec.rb | 136 ------------------------- 3 files changed, 4 insertions(+), 333 deletions(-) delete mode 100644 lib/heroku/api/releases_v3.rb delete mode 100644 spec/heroku/command/fork_spec.rb diff --git a/lib/heroku/api/releases_v3.rb b/lib/heroku/api/releases_v3.rb deleted file mode 100644 index 632f168f7..000000000 --- a/lib/heroku/api/releases_v3.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Heroku - class API - def get_releases_v3(app, range=nil) - headers = { 'Accept' => 'application/vnd.heroku+json; version=3' } - headers.merge!('Range' => range) if range - request( - :expects => [ 200, 206 ], - :headers => headers, - :method => :get, - :path => "/apps/#{app}/releases" - ) - end - - def post_release_v3(app, slug_id, opts={}) - headers = { - 'Accept' => 'application/vnd.heroku+json; version=3', - 'Content-Type' => 'application/json' - } - headers.merge!('Heroku-Deploy-Type' => opts[:deploy_type]) if opts[:deploy_type] - headers.merge!('Heroku-Deploy-Source' => opts[:deploy_source]) if opts[:deploy_source] - - body = { 'slug' => slug_id } - body.merge!('description' => opts[:description]) if opts[:description] - - request( - :expects => 201, - :headers => headers, - :method => :post, - :path => "/apps/#{app}/releases", - :body => Heroku::Helpers.json_encode(body) - ) - end - end -end diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index efe1a0155..b902bee58 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -1,4 +1,3 @@ -require "heroku/api/releases_v3" require "heroku/command/base" module Heroku::Command @@ -14,170 +13,12 @@ class Fork < Base # # -s, --stack STACK # specify a stack for the new app # --region REGION # specify a region + # --copy-pg-data # copy postgres database data instead of just creating empty databases # def index - options[:ignore_no_org] = true - - from = app - to = shift_argument || "#{from}-#{(rand*1000).to_i}" - if from == to - raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") - end - - begin - api.get_app(to).body - error "#{to} app exists.\nUSAGE: heroku fork -a COPY_FROM COPY_TO" - rescue - end - from_info = api.get_app(from).body - - to_info = action("Creating fork #{to}", :org => !!org) do - params = { - "name" => to, - "region" => options[:region] || from_info["region"], - "stack" => options[:stack] || from_info["stack"], - "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] - } - - if org - org_api.post_app(params, org).body - else - api.post_app(params).body - end - end - - action("Copying slug") do - copy_slug(from_info, to_info) - end - - from_config = api.get_config_vars(from).body - from_addons = api.get_addons(from).body - - from_addons.each do |addon| - print "Adding #{addon["name"]}... " - begin - to_addon = api.post_addon(to, addon["name"]).body - puts "done" - rescue Heroku::API::Errors::RequestFailed => ex - puts "skipped (%s)" % json_decode(ex.response.body)["error"] - rescue Heroku::API::Errors::NotFound - puts "skipped (not found)" - end - if addon["name"] =~ /^heroku-postgresql:/ - from_var_name = "#{addon["attachment_name"]}_URL" - from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - if from_config[from_var_name] == from_config["DATABASE_URL"] - from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"] - end - from_config.delete(from_var_name) - - plan = addon["name"].split(":").last - unless %w(dev basic hobby-dev hobby-basic).include? plan - wait_for_db to, to_addon - end - - migrate_db addon, from, to_addon, to - end - end - - to_config = api.get_config_vars(to).body - - action("Copying config vars") do - diff = from_config.inject({}) do |ax, (key, val)| - ax[key] = val unless to_config[key] - ax - end - api.put_config_vars to, diff - end - - puts "Fork complete, view it at #{to_info['web_url']}" - rescue => e - raise if e.is_a?(Heroku::Command::CommandFailed) - - puts "Failed to fork app #{from} to #{to}." - message = "WARNING: Potentially Destructive Action\nThis command will destroy #{to} (including all add-ons)." - - if confirm_command(to, message) - action("Deleting #{to}") do - begin - api.delete_app(to) - rescue Heroku::API::Errors::NotFound - end - end - end - puts "Original exception below:" - raise e + Heroku::JSPlugin.setup + Heroku::JSPlugin.install('heroku-fork') unless Heroku::JSPlugin.is_plugin_installed?('heroku-fork') + Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) end - - private - - def copy_slug(from_info, to_info) - from = from_info["name"] - to = to_info["name"] - from_releases = api.get_releases_v3(from, 'version ..; order=desc,max=1;').body - raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? - from_slug = from_releases.first.fetch('slug', {}) - raise Heroku::Command::CommandFailed.new("No slug on #{from}") unless from_slug - api.post_release_v3(to, - from_slug["id"], - :description => "Forked from #{from}", - :deploy_type => "fork", - :deploy_source => from_info["id"]) - end - - def migrate_db(from_addon, from, to_addon, to) - transfer = nil - - action("Transferring database (this can take some time)") do - from_config = api.get_config_vars(from).body - from_attachment = from_addon["attachment_name"] - to_config = api.get_config_vars(to).body - to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - - resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(to, api) - attachment = resolver.resolve("#{to_attachment}_URL", nil) - pgb = Heroku::Client::HerokuPostgresql.new(attachment) - transfer = pgb.pg_copy( - from_attachment.gsub('HEROKU_POSTGRESQL_',''), - from_config["#{from_attachment}_URL"], - to_attachment.gsub('HEROKU_POSTGRESQL_',''), - to_config["#{to_attachment}_URL"]) - - hpg_app_client = Heroku::Client::HerokuPostgresqlApp.new(to) - begin - transfer = hpg_app_client.transfers_get(transfer[:uuid]) - sleep 1 - end until transfer[:finished_at] - if !transfer[:succeeded] - error "An error occurred and your transfer did not finish." - end - print " " - end - end - - def pg_api - require "rest_client" - host = "postgres-api.heroku.com" - RestClient::Resource.new "https://#{host}/client/v11/databases", Heroku::Auth.user, Heroku::Auth.password - end - - def wait_for_db(app, attachment) - attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) } - attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1] - action("Waiting for database to be ready (this can take some time)") do - loop do - begin - waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"] - break unless waiting - sleep 5 - rescue RestClient::ResourceNotFound - rescue Interrupt - exit 0 - end - end - print " " - end - end - end end diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb deleted file mode 100644 index 9cdf0bd21..000000000 --- a/spec/heroku/command/fork_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -require "heroku/api/releases_v3" -require "spec_helper" -require "heroku/command/fork" - -module Heroku::Command - - describe Fork do - - before(:each) do - stub_core - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - begin - api.delete_app("example-fork") - rescue Heroku::API::Errors::NotFound - end - end - - context "successfully" do - - before(:each) do - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [{"slug" => {"id" => "SLUG_ID"}}], - :status => 206}) - - Excon.stub({ :method => :post, - :path => "/apps/example-fork/releases"}, - { :status => 201}) - end - - after(:each) do - Excon.stubs.shift - Excon.stubs.shift - end - - it "forks an app" do - stderr, stdout = execute("fork example-fork") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Creating fork example-fork... done -Copying slug... done -Copying config vars... done -Fork complete, view it at http://example-fork.herokuapp.com/ -STDOUT - end - - it "copies slug" do - from_info = api.get_app("example").body - expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original - expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", - "SLUG_ID", - :description => "Forked from example", - :deploy_type => "fork", - :deploy_source => from_info["id"]).and_call_original - execute("fork example-fork") - end - - it "copies config vars" do - config_vars = { - "SECRET" => "imasecret", - "FOO" => "bar", - "LANG_ENV" => "production" - } - api.put_config_vars("example", config_vars) - execute("fork example-fork") - expect(api.get_config_vars("example-fork").body).to eq(config_vars) - end - - it "re-provisions add-ons" do - api.post_addon("example", "heroku-postgresql:hobby-dev") - execute("fork example-fork") - expect(api.get_addons("example-fork").body[0]["name"]).to eq("heroku-postgresql:hobby-dev") - end - end - - describe "error handling" do - it "fails if no source release exists" do - begin - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [], - :status => 206}) - execute("fork example-fork") - raise - rescue Heroku::Command::CommandFailed => e - expect(e.message).to eq("No releases on example") - ensure - Excon.stubs.shift - end - end - - it "fails if source slug does not exist" do - begin - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [{"slug" => nil}], - :status => 206}) - execute("fork example-fork") - raise - rescue Heroku::Command::CommandFailed => e - expect(e.message).to eq("No slug on example") - ensure - Excon.stubs.shift - end - end - - it "doesn't attempt to fork to the same app" do - expect do - execute("fork example") - end.to raise_error(Heroku::Command::CommandFailed, /same app/) - end - - it "confirms before deleting the app" do - Excon.stub({:path => "/apps/example/releases"}, {:status => 500}) - begin - execute("fork example-fork") - rescue Heroku::API::Errors::ErrorWithResponse - ensure - Excon.stubs.shift - end - expect(api.get_apps.body.map { |app| app["name"] }).to eq( - %w( example example-fork ) - ) - end - - it "deletes fork app on error, before re-raising" do - stub(Heroku::Command).confirm_command.returns(true) - expect(api.get_apps.body.map { |app| app["name"] }).to eq(%w( example )) - end - end - end -end From 2b778372bb0ec176a5541f8edfd16ca86534e356 Mon Sep 17 00:00:00 2001 From: gsuess Date: Wed, 18 Mar 2015 15:08:15 +0100 Subject: [PATCH 380/952] Properly escape shell variables. --- lib/heroku/command/config.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index 86dfcc223..3bc61d71a 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -1,4 +1,6 @@ require "heroku/command/base" +require "shellwords" + # manage app config vars # @@ -40,7 +42,7 @@ def index vars.each {|key, value| vars[key] = value.to_s} if options[:shell] vars.keys.sort.each do |key| - display(%{#{key}=#{vars[key]}}) + display(%{#{key}=#{Shellwords.shellescape vars[key]}}) end else styled_header("#{app} Config Vars") From 7664808da3d0feac9b8540ac0e66a0e4ca82a988 Mon Sep 17 00:00:00 2001 From: gsuess Date: Wed, 18 Mar 2015 16:11:29 +0100 Subject: [PATCH 381/952] Updated tests for #1454 --- spec/heroku/command/config_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/heroku/command/config_spec.rb b/spec/heroku/command/config_spec.rb index aaf816412..9383be68d 100644 --- a/spec/heroku/command/config_spec.rb +++ b/spec/heroku/command/config_spec.rb @@ -56,12 +56,13 @@ module Heroku::Command end it "shows configs in a shell compatible format" do - api.put_config_vars("example", { 'A' => 'one', 'B' => 'two three' }) + api.put_config_vars("example", { 'A' => 'one', 'B' => 'two three', 'C' => "foo&bar" }) stderr, stdout = execute("config --shell") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT A=one -B=two three +B=two\\ three +C=foo\\&bar STDOUT end From e541dc59a5aafcb0f8566475264b694bbe4a2ba6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Mar 2015 16:21:55 -0700 Subject: [PATCH 382/952] use ssl for v4 download when not on windows --- lib/heroku/jsplugin.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 4cefbd2a0..1e8ee64cc 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -91,7 +91,8 @@ def self.setup return if File.exist? bin $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) - resp = Excon.get(url, :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + resp = Excon.get(url, opts) open(bin, "wb") do |file| file.write(resp.body) end @@ -133,7 +134,16 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/master/manifest.json").body) + @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/master/manifest.json", excon_opts).body) + end + + def self.excon_opts + if os == 'windows' + # S3 SSL downloads do not work from ruby in Windows + {:ssl_verify_peer => false} + else + {} + end end def self.url From 7068191ac92e3b89568abfe0711547e32680cae8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Mar 2015 16:28:50 -0700 Subject: [PATCH 383/952] only show request id on an error --- lib/heroku/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index f6ab05cda..636ec6a5d 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -283,7 +283,7 @@ def error(message, report=false, opts={}) rollbar_id = Rollbar.error(message) if report $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id request_id = opts[:error] && opts[:error].response ? opts[:error].response.headers['Request-Id'] : nil - $stderr.puts "Request ID: #{request_id}" if request_id + $stderr.puts "Request ID: #{request_id}" if report && request_id exit(1) end From 98a6f147093308b25896f875b2cf74335569e363 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Mar 2015 16:49:50 -0700 Subject: [PATCH 384/952] v3.30.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c42f7b08e..04bcdb7cc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.30.0 2015-03-18 +================= +New fork implementation +Only show request ids on error + 3.29.0 2015-03-17 ================= Fixed architecture detection on jruby for jsplugins diff --git a/Gemfile.lock b/Gemfile.lock index 50938cfe7..83f1d95d0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.29.0) + heroku (3.30.0) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.43.0) + excon (0.44.4) fakefs (0.5.4) heroku-api (0.3.22) excon (~> 0.38) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index cea254df7..cfb851a37 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.29.0" + VERSION = "3.30.0" end From 73453784793a3a644346dc4ba5ce7fd1c59e2805 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 10:01:01 -0700 Subject: [PATCH 385/952] updated gems --- Gemfile.lock | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 83f1d95d0..ca3e8c0a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,7 +12,7 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.3.6) + addressable (2.3.7) aws-s3 (0.6.3) builder mime-types @@ -33,35 +33,36 @@ GEM heroku-api (0.3.22) excon (~> 0.38) multi_json (~> 1.8) - json (1.8.1) + json (1.8.2) launchy (2.4.3) addressable (~> 2.3) mime-types (1.25.1) - multi_json (1.10.1) + multi_json (1.11.0) netrc (0.10.3) rake (10.4.2) rest-client (1.6.7) mime-types (>= 1.16) rr (1.1.2) - rspec (3.1.0) - rspec-core (~> 3.1.0) - rspec-expectations (~> 3.1.0) - rspec-mocks (~> 3.1.0) - rspec-core (3.1.7) - rspec-support (~> 3.1.0) - rspec-expectations (3.1.2) + rspec (3.2.0) + rspec-core (~> 3.2.0) + rspec-expectations (~> 3.2.0) + rspec-mocks (~> 3.2.0) + rspec-core (3.2.2) + rspec-support (~> 3.2.0) + rspec-expectations (3.2.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.1.0) - rspec-mocks (3.1.3) - rspec-support (~> 3.1.0) - rspec-support (3.1.2) + rspec-support (~> 3.2.0) + rspec-mocks (3.2.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.2.0) + rspec-support (3.2.2) rubyzip (0.9.9) safe_yaml (1.0.4) - simplecov (0.9.1) + simplecov (0.9.2) docile (~> 1.1.0) multi_json (~> 1.0) - simplecov-html (~> 0.8.0) - simplecov-html (0.8.0) + simplecov-html (~> 0.9.0) + simplecov-html (0.9.0) term-ansicolor (1.2.2) tins (~> 0.8) thor (0.18.1) @@ -69,7 +70,7 @@ GEM webmock (1.20.4) addressable (>= 2.3.6) crack (>= 0.3.2) - xml-simple (1.1.4) + xml-simple (1.1.5) PLATFORMS ruby From 572c401a258b56ed24b2482bf4bf5cb9478cb338 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 10:21:50 -0700 Subject: [PATCH 386/952] cleaned up text and removed pending spec from open_ssl specs --- lib/heroku/open_ssl.rb | 34 +++++++------- spec/heroku/open_ssl_spec.rb | 89 +++++++++++++++--------------------- 2 files changed, 54 insertions(+), 69 deletions(-) diff --git a/lib/heroku/open_ssl.rb b/lib/heroku/open_ssl.rb index e1941be6c..f6a8f70cb 100644 --- a/lib/heroku/open_ssl.rb +++ b/lib/heroku/open_ssl.rb @@ -10,21 +10,21 @@ def self.openssl(*args) system(openssl, *args) end end - + def self.openssl=(val) @checked = false ENV["OPENSSL"] = val end - + class CertificateRequest attr_accessor :domain, :subject, :key_size, :self_signed - + def initialize() @key_size = 2048 @self_signed = false super end - + def generate if self_signed generate_self_signed_certificate @@ -32,46 +32,46 @@ def generate generate_csr end end - + class Result attr_accessor :request, :key_file, :csr_file, :crt_file - + def initialize(request, key_file, csr_file, crt_file) @request = request.dup @key_file, @csr_file, @crt_file = key_file, csr_file, crt_file end end - - private + + private def generate_csr keyfile = "#{domain}.key" csrfile = "#{domain}.csr" - + openssl_req_new(keyfile, csrfile) or raise GenericError, "Key and CSR generation failed: #{$?}" - + return Result.new(self, keyfile, csrfile, nil) end - + def generate_self_signed_certificate keyfile = "#{domain}.key" crtfile = "#{domain}.crt" - + openssl_req_new(keyfile, crtfile, "-x509") or raise GenericError, "Key and self-signed certificate generation failed: #{$?}" - + return Result.new(self, keyfile, nil, crtfile) end - + def openssl_req_new(keyfile, outfile, *args) Heroku::OpenSSL.ensure_openssl_installed! Heroku::OpenSSL.openssl("req", "-new", "-newkey", "rsa:#{key_size}", "-nodes", "-keyout", keyfile, "-out", outfile, "-subj", subject, *args) end end - + class GenericError < StandardError; end class NotInstalledError < GenericError include Heroku::Helpers - + def installation_hint if running_on_a_mac? "With Homebrew installed, run the following command:\n$ brew install openssl" @@ -83,7 +83,7 @@ def installation_hint end end end - + def self.ensure_openssl_installed! return if @checked openssl("version") or raise NotInstalledError diff --git a/spec/heroku/open_ssl_spec.rb b/spec/heroku/open_ssl_spec.rb index 71c3f7083..9956db669 100644 --- a/spec/heroku/open_ssl_spec.rb +++ b/spec/heroku/open_ssl_spec.rb @@ -1,7 +1,7 @@ require "heroku/open_ssl" describe Heroku::OpenSSL do - # This undoes any temporary changes to the property, and also + # This undoes any temporary changes to the property, and also # resets the flag indicating the path has already been checked. before(:all) do Heroku::OpenSSL.openssl = nil @@ -9,7 +9,7 @@ after(:each) do Heroku::OpenSSL.openssl = nil end - + describe :openssl do it "returns 'openssl' when nothing else is set" do expect(Heroku::OpenSSL.openssl).to eq("openssl") @@ -20,34 +20,34 @@ expect(Heroku::OpenSSL.openssl).to eq('/usr/bin/openssl') ENV['OPENSSL'] = nil end - + it "can be set with openssl=" do Heroku::OpenSSL.openssl = '/usr/local/bin/openssl' expect(Heroku::OpenSSL.openssl).to eq('/usr/local/bin/openssl') Heroku::OpenSSL.openssl = nil end - + it "runs openssl(1) when passed arguments" do expect(Heroku::OpenSSL).to receive(:system).with("openssl", "version").and_return(true) expect(Heroku::OpenSSL.openssl("version")).to be true end end - + describe :ensure_openssl_installed! do it "calls openssl(1) to ensure it's available" do expect(Heroku::OpenSSL).to receive(:openssl).with("version").and_return(true) Heroku::OpenSSL.ensure_openssl_installed! end - + it "detects openssl(1) is available when it is available" do expect { Heroku::OpenSSL.ensure_openssl_installed! }.not_to raise_error end - + it "detects openssl(1) is absent when it isn't available" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) end - + it "gives good installation advice on a Mac" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| @@ -56,7 +56,7 @@ expect(ex.installation_hint).to match(/brew install openssl/) } end - + it "gives good installation advice on Windows" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| @@ -65,7 +65,7 @@ expect(ex.installation_hint).to match(/Win32OpenSSL\.html/) } end - + it "gives good installation advice on miscellaneous Unixen" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' expect { Heroku::OpenSSL.ensure_openssl_installed! }.to raise_error(Heroku::OpenSSL::NotInstalledError) { |ex| @@ -75,135 +75,120 @@ } end end - - describe :CertificateRequest do + + describe :certificate_request do it "initializes with good defaults" do request = Heroku::OpenSSL::CertificateRequest.new expect(request).not_to be_nil expect(request.key_size).to eq(2048) expect(request.self_signed).to be false end - + context "generating with self_signed off" do before(:all) do @prev_dir = Dir.getwd @dir = Dir.mktmpdir Dir.chdir @dir - + request = Heroku::OpenSSL::CertificateRequest.new request.domain = 'example.com' request.subject = '/CN=example.com' - + # Would like to do this, but the current version of rspec doesn't support it # expect { result = request.generate }.to output.to_stdout_from_any_process @result = request.generate end - + it "should create Result object" do expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result end - + it "should have a key filename" do expect(@result.key_file).to eq('example.com.key') end - + it "should have a CSR filename" do expect(@result.csr_file).to eq('example.com.csr') end - + it "should not have a certificate filename" do expect(@result.crt_file).to be_nil end - + it "should produce a PEM key file" do expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) end - + it "should produce a PEM certificate file" do expect(File.read(@result.csr_file)).to start_with("-----BEGIN CERTIFICATE REQUEST-----\n") end - + after(:all) do Dir.chdir @prev_dir FileUtils.remove_entry_secure @dir end end - + context "generating with self_signed on" do before(:all) do @prev_dir = Dir.getwd @dir = Dir.mktmpdir Dir.chdir @dir - + request = Heroku::OpenSSL::CertificateRequest.new request.domain = 'example.com' request.subject = '/CN=example.com' request.self_signed = true - + # Would like to do this, but the current version of rspec doesn't support it # expect { result = request.generate }.to output.to_stdout_from_any_process @result = request.generate end - + it "should create Result object" do expect(@result).to be_kind_of Heroku::OpenSSL::CertificateRequest::Result end - + it "should have a key filename" do expect(@result.key_file).to eq('example.com.key') end - + it "should not have a CSR filename" do expect(@result.csr_file).to be_nil end - + it "should have a certificate filename" do expect(@result.crt_file).to eq('example.com.crt') end - + it "should produce a PEM key file" do expect(File.read(@result.key_file)).to match(/\A-----BEGIN (RSA )?PRIVATE KEY-----\n/) end - + it "should produce a PEM certificate file" do expect(File.read(@result.crt_file)).to start_with("-----BEGIN CERTIFICATE-----\n") end - + after(:all) do Dir.chdir @prev_dir FileUtils.remove_entry_secure @dir end end - + it "raises installation error when openssl(1) isn't installed" do Heroku::OpenSSL.openssl = 'openssl-THIS-FILE-SHOULD-NOT-EXIST' - + Dir.mktmpdir do |dir| Dir.chdir(dir) do request = Heroku::OpenSSL::CertificateRequest.new request.domain = 'example.com' request.subject = '/CN=example.com' - - expect { result = request.generate }.to raise_error(Heroku::OpenSSL::NotInstalledError) + + expect { request.generate }.to raise_error(Heroku::OpenSSL::NotInstalledError) end end - + Heroku::OpenSSL.openssl = nil end - - it "uses key_size to control the key's size" do - skip "Can't be tested without an rspec supporting to_stdout_from_any_process" unless RSpec::Matchers::BuiltIn::Output.method_defined? :to_stdout_from_any_process - - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - request = Heroku::OpenSSL::CertificateRequest.new - request.domain = 'example.com' - request.subject = '/CN=example.com' - request.key_size = 4096 - - expect { result = request.generate }.to output(/Generating a 4096 bit RSA private key/).to_stdout_from_any_process - end - end - end end end From 820096b45c2fc62ae052423811d538dc7412adcf Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 10:47:14 -0700 Subject: [PATCH 387/952] updated excon and heroku-api gems --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ca3e8c0a5..b4de09fa6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -30,8 +30,8 @@ GEM docile (1.1.5) excon (0.44.4) fakefs (0.5.4) - heroku-api (0.3.22) - excon (~> 0.38) + heroku-api (0.3.23) + excon (~> 0.44) multi_json (~> 1.8) json (1.8.2) launchy (2.4.3) From 6244c1b7ff4fe2035a08674e0ec4446a9dc8ff91 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 11:52:31 -0700 Subject: [PATCH 388/952] v3.30.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 04bcdb7cc..b1e843f9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.30.1 2015-03-19 +================= +Updated gems + 3.30.0 2015-03-18 ================= New fork implementation diff --git a/Gemfile.lock b/Gemfile.lock index b4de09fa6..d917226d3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.0) + heroku (3.30.1) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index cfb851a37..8ef8186ef 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.0" + VERSION = "3.30.1" end From 0cd7fc1d0cd37015768c0c049671d797db79e2dd Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 12:25:48 -0700 Subject: [PATCH 389/952] added updater for v4 plugins --- lib/heroku/command/update.rb | 4 ++++ lib/heroku/jsplugin.rb | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 377339b35..ce4306b91 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -17,6 +17,10 @@ class Heroku::Command::Update < Heroku::Command::Base def index validate_arguments! update_from_url(false) + if Heroku::JSPlugin.setup? + display("Updating Toolbelt v4...") + Heroku::JSPlugin.update + end end # update:beta diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 1e8ee64cc..571bc572d 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -75,6 +75,10 @@ def self.uninstall(name) system "#{bin} plugins:uninstall #{name}" end + def self.update + system "#{bin} update" + end + def self.version `#{bin} version` end From fed138c97b4661295842d931ff44fb410298d9c2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 12:27:01 -0700 Subject: [PATCH 390/952] v3.30.2. --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b1e843f9c..56468f936 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.30.2 2015-03-19 +================= +Made updater also update v4 if it is setup + 3.30.1 2015-03-19 ================= Updated gems diff --git a/Gemfile.lock b/Gemfile.lock index d917226d3..25dd47ba0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.1) + heroku (3.30.2) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 8ef8186ef..b9641257c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.1" + VERSION = "3.30.2" end From ed78316e3d6c0f7c330f9ab11422d24a73e5a684 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 12:49:44 -0700 Subject: [PATCH 391/952] Revert "Merge pull request #1455 from heroku/request-id" This reverts commit 9d34938c9b380910c850318e83338c41e3ecf8e0, reversing changes made to aebbb855b825c721e5fa09a9430a8fa64707c290. --- lib/heroku/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 636ec6a5d..f6ab05cda 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -283,7 +283,7 @@ def error(message, report=false, opts={}) rollbar_id = Rollbar.error(message) if report $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id request_id = opts[:error] && opts[:error].response ? opts[:error].response.headers['Request-Id'] : nil - $stderr.puts "Request ID: #{request_id}" if report && request_id + $stderr.puts "Request ID: #{request_id}" if request_id exit(1) end From 21827c436ee232fe2090adf482f4062708da8325 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 19 Mar 2015 12:50:14 -0700 Subject: [PATCH 392/952] Revert "Merge pull request #1450 from shirshendu/master" This reverts commit ff0b6afa6a648ac7ea8a51244b9dd1f62f44e664, reversing changes made to f9fe50ccdb8281816119be4d59fde3bcdb07f7a9. --- lib/heroku/command.rb | 12 ++++++------ lib/heroku/helpers.rb | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index e4656e376..3d7bb00f4 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -227,13 +227,13 @@ def self.run(cmd, arguments=[]) rescue Heroku::API::Errors::VerificationRequired, RestClient::PaymentRequired => e retry if Heroku::Helpers.confirm_billing rescue Heroku::API::Errors::NotFound => e - error(extract_error(e.response.body) { + error extract_error(e.response.body) { e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - }, nil, :error => e) + } rescue RestClient::ResourceNotFound => e - error(extract_error(e.http_body) { + error extract_error(e.http_body) { e.http_body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" - }, nil, :error => e) + } rescue Heroku::API::Errors::Locked => e app = e.response.headers["X-Confirmation-Required"] if confirm_command(app, extract_error(e.response.body)) @@ -257,10 +257,10 @@ def self.run(cmd, arguments=[]) end retry else - error extract_error(e.response.body), nil, :error => e + error extract_error(e.response.body) end rescue Heroku::API::Errors::ErrorWithResponse => e - error extract_error(e.response.body), nil, :error => e + error extract_error(e.response.body) rescue RestClient::RequestFailed => e if e.response.code == 403 && e.response.headers.has_key?(:heroku_two_factor_required) Heroku::Auth.preauth diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index f6ab05cda..6051c86d0 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -274,7 +274,7 @@ def output_with_bang(message="", new_line=true) display(format_with_bang(message), new_line) end - def error(message, report=false, opts={}) + def error(message, report=false) if Heroku::Helpers.error_with_failure display("failed") Heroku::Helpers.error_with_failure = false @@ -282,8 +282,6 @@ def error(message, report=false, opts={}) $stderr.puts(format_with_bang(message)) rollbar_id = Rollbar.error(message) if report $stderr.puts("Error ID: #{rollbar_id}") if rollbar_id - request_id = opts[:error] && opts[:error].response ? opts[:error].response.headers['Request-Id'] : nil - $stderr.puts "Request ID: #{request_id}" if request_id exit(1) end From d94111cd87f8f8d94a9390596c2e00846dc14237 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 19 Mar 2015 13:24:52 -0700 Subject: [PATCH 393/952] Add some basic pg:copy specs --- spec/heroku/command/pg_backups_spec.rb | 125 +++++++++++++++++++++++++ spec/spec_helper.rb | 10 ++ 2 files changed, 135 insertions(+) create mode 100644 spec/heroku/command/pg_backups_spec.rb diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb new file mode 100644 index 000000000..e9df7776d --- /dev/null +++ b/spec/heroku/command/pg_backups_spec.rb @@ -0,0 +1,125 @@ +require "spec_helper" +require "heroku/command/pg" +require "heroku/command/pg_backups" + +module Heroku::Command + describe Pg do + let(:ivory_url) { 'postgres:///database_url' } + let(:green_url) { 'postgres:///green_database_url' } + let(:red_url) { 'postgres:///red_database_url' } + + let(:teal_url) { 'postgres:///teal_database_url' } + + let(:example_attachments) do + [ + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => ivory_url, + 'type' => 'heroku-postgresql:standard-0' }}), + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_GREEN', + 'config_var' => 'HEROKU_POSTGRESQL_GREEN_URL', + 'resource' => {'name' => 'softly-mocking-123', + 'value' => green_url, + 'type' => 'heroku-postgresql:standard-0' }}), + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_RED', + 'config_var' => 'HEROKU_POSTGRESQL_RED_URL', + 'resource' => {'name' => 'whatever-something-2323', + 'value' => red_url, + 'type' => 'heroku-postgresql:standard-0' }}) + ] + end + + let(:aux_example_attachments) do + [ + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'aux-example'}, + 'name' => 'HEROKU_POSTGRESQL_TEAL', + 'config_var' => 'HEROKU_POSTGRESQL_TEAL_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => teal_url, + 'type' => 'heroku-postgresql:standard-0' }}) + ] + end + + before do + stub_core + + api.post_app "name" => "example" + api.put_config_vars "example", { + "DATABASE_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_IVORY_URL" => ivory_url, + "HEROKU_POSTGRESQL_GREEN_URL" => green_url, + "HEROKU_POSTGRESQL_RED_URL" => red_url, + } + + api.post_app "name" => "aux-example" + api.put_config_vars "aux-example", { + "DATABASE_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_TEAL_URL" => teal_url + } + end + + after do + api.delete_app "aux-example" + api.delete_app "example" + end + + describe "heroku pg:copy" do + let(:copy_info) do + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'pg_dump', :to_type => 'pg_restore', + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true } + end + + before do + # hideous hack because we can't do dependency injection + orig_new = Heroku::Helpers::HerokuPostgresql::Resolver.method(:new) + allow(Heroku::Helpers::HerokuPostgresql::Resolver).to receive(:new) do |app_name, api| + resolver = orig_new.call(app_name, api) + allow(resolver).to receive(:app_attachments) do + if resolver.app_name == 'example' + example_attachments + else + aux_example_attachments + end + end + resolver + end + end + + it "copies data from one database to another" do + stub_pg.pg_copy('IVORY', ivory_url, 'RED', red_url).returns(copy_info) + stub_pgapp.transfers_get.returns(copy_info) + + stderr, stdout = execute("pg:copy ivory red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Copy completed/) + end + + it "does not copy without confirmation" do + stderr, stdout = execute("pg:copy ivory red") + expect(stderr).to match(/Confirmation did not match example. Aborted./) + expect(stdout).to match(/WARNING: Destructive Action/) + expect(stdout).to match(/This command will affect the app: example/) + expect(stdout).to match(/To proceed, type "example" or re-run this command with --confirm example/) + end + + it "copies across apps" do + stub_pg.pg_copy('TEAL', teal_url, 'RED', red_url).returns(copy_info) + stub_pgapp.transfers_get.returns(copy_info) + + stderr, stdout = execute("pg:copy aux-example::teal red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Copy completed/) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 39f756f0b..fba558d9f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -146,6 +146,16 @@ def stub_pg end end +def stub_pgapp + @stubbed_pgapp ||= begin + stubbed_pgapp = nil + any_instance_of(Heroku::Client::HerokuPostgresqlApp) do |pg| + stubbed_pgapp = stub(pg) + end + stubbed_pgapp + end +end + def stub_pgbackups @stubbed_pgbackups ||= begin stubbed_pgbackups = nil From cf72c25861c6de47cfc3f9a926c62689cdbb48fd Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 19 Mar 2015 13:32:10 -0700 Subject: [PATCH 394/952] Avoid nil error when no backup source size info available --- lib/heroku/command/pg_backups.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 68d4b6f8a..22e4f5610 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -224,8 +224,9 @@ def backup_status else "Manual" end - orig_size = backup[:source_bytes] backup_size = backup[:processed_bytes] + orig_size = backup[:source_bytes] || backup_size + compression_pct = [((orig_size - backup_size).to_f / orig_size * 100).round, 0].max display <<-EOF === Backup info: #{backup_id} From 15c64db4f49545daff31829c1f7dd09896d92ed1 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 19 Mar 2015 15:40:42 -0700 Subject: [PATCH 395/952] Add basic specs for pg:backups info and minor fixes --- lib/heroku/command/pg_backups.rb | 3 +- spec/heroku/command/pg_backups_spec.rb | 69 ++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 22e4f5610..466c671c8 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -96,7 +96,7 @@ def transfer_name(backup_num, prefix='b') end def backup_num(transfer_name) - /b(\d+)/.match(transfer_name) && $1 + /b(\d+)/.match(transfer_name) && $1.to_i end def transfer_status(t) @@ -199,6 +199,7 @@ def backup_status if last_backup.nil? error("No backups. Capture one with `heroku pg:backups capture`.") else + backup_id = transfer_name(last_backup[:num]) if verbose client.transfers_get(last_backup[:num], verbose) else diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index e9df7776d..9ac8bdbe2 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -121,5 +121,74 @@ module Heroku::Command expect(stdout).to match(/Copy completed/) end end + + describe "heroku pg:backups info" do + let(:logged_at) { Time.now } + let(:started_at) { Time.now } + let(:finished_at) { Time.now } + let(:from_name) { 'RED' } + let(:source_size) { 42 } + let(:backup_size) { source_size / 2 } + + let(:logs) { [{ 'created_at' => logged_at, 'message' => "hello world" }] } + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 1, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', + :from_name => from_name, :to_name => 'BACKUP', + :num => 2, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true } + ] + end + + before do + stub_pgapp.transfers_get(2, true).returns(transfers.find { |xfer| xfer[:num] == 2 }) + stub_pgapp.transfers_get(1, true).returns(transfers.find { |xfer| xfer[:num] == 1 }) + stub_pgapp.transfers.returns(transfers) + end + + it "displays info for the given backup" do + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B (50% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "defaults to the latest backup if no specific backup provided" do + stderr, stdout = execute("pg:backups info") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b002 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B (50% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + end end end From f7ea917d9b707eeafd7f4be00cc193e8796de152 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 19 Mar 2015 18:08:16 -0700 Subject: [PATCH 396/952] Add basic specs for public-url; make backup argument optional Now, heroku pg:backups public-url with no argument will now generate a URL for the latest backup, as with pgbackups:url. This also moves the auxiliary information to stdout so that the stdout output of the command can be used directly to download the backup (e.g., with curl) or restore from it. --- lib/heroku/command/pg_backups.rb | 21 ++++++++-- spec/heroku/command/pg_backups_spec.rb | 55 +++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 466c671c8..675c0f102 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -381,9 +381,24 @@ def public_url backup_id = shift_argument validate_arguments! - url_info = hpg_app_client(app).transfers_public_url(backup_num(backup_id)) - display "The following URL will expire at #{url_info[:expires_at]}:" - display " '#{url_info[:url]}'" + backup_num = nil + client = hpg_app_client(app) + if backup_id + backup_num = backup_num(backup_id) + else + last_successful_backup = client.transfers.select do |xfer| + xfer[:succeeded] && xfer[:to_type] == 'gof3r' + end.sort_by { |b| b[:num] }.last + if last_successful_backup.nil? + error("No backups. Capture one with `heroku pg:backups capture`.") + else + backup_num = last_successful_backup[:num] + end + end + + url_info = client.transfers_public_url(backup_num) + $stderr.puts "The following URL will expire at #{url_info[:expires_at]}:" + display url_info[:url] end def cancel_backup diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 9ac8bdbe2..a73e9b6d9 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -173,7 +173,7 @@ module Heroku::Command EOF end - it "defaults to the latest backup if no specific backup provided" do + it "defaults to the latest backup if none is specified" do stderr, stdout = execute("pg:backups info") expect(stderr).to be_empty expect(stdout).to eq <<-EOF @@ -190,5 +190,58 @@ module Heroku::Command EOF end end + + describe "heroku pg:backups public-url" do + let(:logged_at) { Time.now } + let(:started_at) { Time.now } + let(:finished_at) { Time.now } + let(:from_name) { 'RED' } + let(:source_size) { 42 } + let(:backup_size) { source_size / 2 } + + let(:logs) { [{ 'created_at' => logged_at, 'message' => "hello world" }] } + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 1, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', + :from_name => from_name, :to_name => 'BACKUP', + :num => 2, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true } + ] + end + let(:url1_info) do + { :url => 'https://example.com/my-backup', :expires_at => Time.now } + end + let(:url2_info) do + { :url => 'https://example.com/my-other-backup', :expires_at => Time.now } + end + + before do + stub_pgapp.transfers.returns(transfers) + stub_pgapp.transfers_public_url(1).returns(url1_info) + stub_pgapp.transfers_public_url(2).returns(url2_info) + end + + it "gets a public url for the specified backup" do + stderr, stdout = execute("pg:backups public-url b001") + expect(stdout.chomp).to eq url1_info[:url] + expect(stderr).to match(/will expire at #{url1_info[:expires_at]}/) + end + + it "defaults to the latest backup if none is specified" do + stderr, stdout = execute("pg:backups public-url") + expect(stdout.chomp).to eq url2_info[:url] + expect(stderr).to match(/will expire at #{url2_info[:expires_at]}/) + end + end end end From 7068bbdfaa57f7dad3dd6aa3fa12493cbd7594ab Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Fri, 20 Mar 2015 02:16:30 -0700 Subject: [PATCH 397/952] Fix regexp handling in specs --- spec/heroku/command/pg_backups_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index a73e9b6d9..0f827d233 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -234,13 +234,13 @@ module Heroku::Command it "gets a public url for the specified backup" do stderr, stdout = execute("pg:backups public-url b001") expect(stdout.chomp).to eq url1_info[:url] - expect(stderr).to match(/will expire at #{url1_info[:expires_at]}/) + expect(stderr).to match(/will expire at #{Regexp.quote(url1_info[:expires_at].to_s)}/) end it "defaults to the latest backup if none is specified" do stderr, stdout = execute("pg:backups public-url") expect(stdout.chomp).to eq url2_info[:url] - expect(stderr).to match(/will expire at #{url2_info[:expires_at]}/) + expect(stderr).to match(/will expire at #{Regexp.quote(url2_info[:expires_at].to_s)}/) end end end From 71994f1ae1373bcf450a5afd09b977a8433810ce Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Fri, 20 Mar 2015 02:37:30 -0700 Subject: [PATCH 398/952] Fix size_pretty on 1.8.7 since it doesn't guarantee hash key order --- lib/heroku/command/pg_backups.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 675c0f102..d711d26f6 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -112,13 +112,13 @@ def transfer_status(t) end def size_pretty(bytes) - suffixes = { - 'B' => 1, - 'kB' => 1_000, - 'MB' => 1_000_000, - 'GB' => 1_000_000_000, - 'TB' => 1_000_000_000_000 # (ohdear) - } + suffixes = [ + ['B', 1], + ['kB', 1_000], + ['MB', 1_000_000], + ['GB', 1_000_000_000], + ['TB', 1_000_000_000_000] # (ohdear) + ] suffix, multiplier = suffixes.find do |k,v| normalized = bytes / v.to_f normalized >= 0 && normalized < 1_000 From 412e14765a61e28dc4b0847af5dd5f1acb940473 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Fri, 20 Mar 2015 17:28:34 -0700 Subject: [PATCH 399/952] Rename cedar => cedar-10 in help text --- lib/heroku/command/stack.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/stack.rb b/lib/heroku/command/stack.rb index 25f7bbda5..7036590d0 100644 --- a/lib/heroku/command/stack.rb +++ b/lib/heroku/command/stack.rb @@ -13,7 +13,7 @@ class Stack < Base # # $ heroku stack # === example Available Stacks - # cedar + # cedar-10 # * cedar-14 # def index From 13d59bc9457e9a4c47ebcaa1565e44e70a3bc889 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 21 Mar 2015 12:10:55 -0700 Subject: [PATCH 400/952] Revert "use toolbelt v4 for fork" --- lib/heroku/api/releases_v3.rb | 34 +++++++ lib/heroku/command/fork.rb | 167 ++++++++++++++++++++++++++++++- lib/heroku/jsplugin.rb | 14 +-- spec/heroku/command/fork_spec.rb | 136 +++++++++++++++++++++++++ 4 files changed, 335 insertions(+), 16 deletions(-) create mode 100644 lib/heroku/api/releases_v3.rb create mode 100644 spec/heroku/command/fork_spec.rb diff --git a/lib/heroku/api/releases_v3.rb b/lib/heroku/api/releases_v3.rb new file mode 100644 index 000000000..632f168f7 --- /dev/null +++ b/lib/heroku/api/releases_v3.rb @@ -0,0 +1,34 @@ +module Heroku + class API + def get_releases_v3(app, range=nil) + headers = { 'Accept' => 'application/vnd.heroku+json; version=3' } + headers.merge!('Range' => range) if range + request( + :expects => [ 200, 206 ], + :headers => headers, + :method => :get, + :path => "/apps/#{app}/releases" + ) + end + + def post_release_v3(app, slug_id, opts={}) + headers = { + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Content-Type' => 'application/json' + } + headers.merge!('Heroku-Deploy-Type' => opts[:deploy_type]) if opts[:deploy_type] + headers.merge!('Heroku-Deploy-Source' => opts[:deploy_source]) if opts[:deploy_source] + + body = { 'slug' => slug_id } + body.merge!('description' => opts[:description]) if opts[:description] + + request( + :expects => 201, + :headers => headers, + :method => :post, + :path => "/apps/#{app}/releases", + :body => Heroku::Helpers.json_encode(body) + ) + end + end +end diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index b902bee58..efe1a0155 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -1,3 +1,4 @@ +require "heroku/api/releases_v3" require "heroku/command/base" module Heroku::Command @@ -13,12 +14,170 @@ class Fork < Base # # -s, --stack STACK # specify a stack for the new app # --region REGION # specify a region - # --copy-pg-data # copy postgres database data instead of just creating empty databases # def index - Heroku::JSPlugin.setup - Heroku::JSPlugin.install('heroku-fork') unless Heroku::JSPlugin.is_plugin_installed?('heroku-fork') - Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) + options[:ignore_no_org] = true + + from = app + to = shift_argument || "#{from}-#{(rand*1000).to_i}" + if from == to + raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") + end + + begin + api.get_app(to).body + error "#{to} app exists.\nUSAGE: heroku fork -a COPY_FROM COPY_TO" + rescue + end + from_info = api.get_app(from).body + + to_info = action("Creating fork #{to}", :org => !!org) do + params = { + "name" => to, + "region" => options[:region] || from_info["region"], + "stack" => options[:stack] || from_info["stack"], + "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] + } + + if org + org_api.post_app(params, org).body + else + api.post_app(params).body + end + end + + action("Copying slug") do + copy_slug(from_info, to_info) + end + + from_config = api.get_config_vars(from).body + from_addons = api.get_addons(from).body + + from_addons.each do |addon| + print "Adding #{addon["name"]}... " + begin + to_addon = api.post_addon(to, addon["name"]).body + puts "done" + rescue Heroku::API::Errors::RequestFailed => ex + puts "skipped (%s)" % json_decode(ex.response.body)["error"] + rescue Heroku::API::Errors::NotFound + puts "skipped (not found)" + end + if addon["name"] =~ /^heroku-postgresql:/ + from_var_name = "#{addon["attachment_name"]}_URL" + from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] + if from_config[from_var_name] == from_config["DATABASE_URL"] + from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"] + end + from_config.delete(from_var_name) + + plan = addon["name"].split(":").last + unless %w(dev basic hobby-dev hobby-basic).include? plan + wait_for_db to, to_addon + end + + migrate_db addon, from, to_addon, to + end + end + + to_config = api.get_config_vars(to).body + + action("Copying config vars") do + diff = from_config.inject({}) do |ax, (key, val)| + ax[key] = val unless to_config[key] + ax + end + api.put_config_vars to, diff + end + + puts "Fork complete, view it at #{to_info['web_url']}" + rescue => e + raise if e.is_a?(Heroku::Command::CommandFailed) + + puts "Failed to fork app #{from} to #{to}." + message = "WARNING: Potentially Destructive Action\nThis command will destroy #{to} (including all add-ons)." + + if confirm_command(to, message) + action("Deleting #{to}") do + begin + api.delete_app(to) + rescue Heroku::API::Errors::NotFound + end + end + end + puts "Original exception below:" + raise e end + + private + + def copy_slug(from_info, to_info) + from = from_info["name"] + to = to_info["name"] + from_releases = api.get_releases_v3(from, 'version ..; order=desc,max=1;').body + raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? + from_slug = from_releases.first.fetch('slug', {}) + raise Heroku::Command::CommandFailed.new("No slug on #{from}") unless from_slug + api.post_release_v3(to, + from_slug["id"], + :description => "Forked from #{from}", + :deploy_type => "fork", + :deploy_source => from_info["id"]) + end + + def migrate_db(from_addon, from, to_addon, to) + transfer = nil + + action("Transferring database (this can take some time)") do + from_config = api.get_config_vars(from).body + from_attachment = from_addon["attachment_name"] + to_config = api.get_config_vars(to).body + to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] + + resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(to, api) + attachment = resolver.resolve("#{to_attachment}_URL", nil) + pgb = Heroku::Client::HerokuPostgresql.new(attachment) + transfer = pgb.pg_copy( + from_attachment.gsub('HEROKU_POSTGRESQL_',''), + from_config["#{from_attachment}_URL"], + to_attachment.gsub('HEROKU_POSTGRESQL_',''), + to_config["#{to_attachment}_URL"]) + + hpg_app_client = Heroku::Client::HerokuPostgresqlApp.new(to) + begin + transfer = hpg_app_client.transfers_get(transfer[:uuid]) + sleep 1 + end until transfer[:finished_at] + if !transfer[:succeeded] + error "An error occurred and your transfer did not finish." + end + print " " + end + end + + def pg_api + require "rest_client" + host = "postgres-api.heroku.com" + RestClient::Resource.new "https://#{host}/client/v11/databases", Heroku::Auth.user, Heroku::Auth.password + end + + def wait_for_db(app, attachment) + attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) } + attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1] + action("Waiting for database to be ready (this can take some time)") do + loop do + begin + waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"] + break unless waiting + sleep 5 + rescue RestClient::ResourceNotFound + rescue Interrupt + exit 0 + end + end + print " " + end + end + end end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 571bc572d..9608561fb 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -95,8 +95,7 @@ def self.setup return if File.exist? bin $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) - opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) - resp = Excon.get(url, opts) + resp = Excon.get(url, :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) open(bin, "wb") do |file| file.write(resp.body) end @@ -138,16 +137,7 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/master/manifest.json", excon_opts).body) - end - - def self.excon_opts - if os == 'windows' - # S3 SSL downloads do not work from ruby in Windows - {:ssl_verify_peer => false} - else - {} - end + @manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/master/manifest.json").body) end def self.url diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb new file mode 100644 index 000000000..9cdf0bd21 --- /dev/null +++ b/spec/heroku/command/fork_spec.rb @@ -0,0 +1,136 @@ +require "heroku/api/releases_v3" +require "spec_helper" +require "heroku/command/fork" + +module Heroku::Command + + describe Fork do + + before(:each) do + stub_core + api.post_app("name" => "example", "stack" => "cedar") + end + + after(:each) do + api.delete_app("example") + begin + api.delete_app("example-fork") + rescue Heroku::API::Errors::NotFound + end + end + + context "successfully" do + + before(:each) do + Excon.stub({ :method => :get, + :path => "/apps/example/releases" }, + { :body => [{"slug" => {"id" => "SLUG_ID"}}], + :status => 206}) + + Excon.stub({ :method => :post, + :path => "/apps/example-fork/releases"}, + { :status => 201}) + end + + after(:each) do + Excon.stubs.shift + Excon.stubs.shift + end + + it "forks an app" do + stderr, stdout = execute("fork example-fork") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Creating fork example-fork... done +Copying slug... done +Copying config vars... done +Fork complete, view it at http://example-fork.herokuapp.com/ +STDOUT + end + + it "copies slug" do + from_info = api.get_app("example").body + expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original + expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", + "SLUG_ID", + :description => "Forked from example", + :deploy_type => "fork", + :deploy_source => from_info["id"]).and_call_original + execute("fork example-fork") + end + + it "copies config vars" do + config_vars = { + "SECRET" => "imasecret", + "FOO" => "bar", + "LANG_ENV" => "production" + } + api.put_config_vars("example", config_vars) + execute("fork example-fork") + expect(api.get_config_vars("example-fork").body).to eq(config_vars) + end + + it "re-provisions add-ons" do + api.post_addon("example", "heroku-postgresql:hobby-dev") + execute("fork example-fork") + expect(api.get_addons("example-fork").body[0]["name"]).to eq("heroku-postgresql:hobby-dev") + end + end + + describe "error handling" do + it "fails if no source release exists" do + begin + Excon.stub({ :method => :get, + :path => "/apps/example/releases" }, + { :body => [], + :status => 206}) + execute("fork example-fork") + raise + rescue Heroku::Command::CommandFailed => e + expect(e.message).to eq("No releases on example") + ensure + Excon.stubs.shift + end + end + + it "fails if source slug does not exist" do + begin + Excon.stub({ :method => :get, + :path => "/apps/example/releases" }, + { :body => [{"slug" => nil}], + :status => 206}) + execute("fork example-fork") + raise + rescue Heroku::Command::CommandFailed => e + expect(e.message).to eq("No slug on example") + ensure + Excon.stubs.shift + end + end + + it "doesn't attempt to fork to the same app" do + expect do + execute("fork example") + end.to raise_error(Heroku::Command::CommandFailed, /same app/) + end + + it "confirms before deleting the app" do + Excon.stub({:path => "/apps/example/releases"}, {:status => 500}) + begin + execute("fork example-fork") + rescue Heroku::API::Errors::ErrorWithResponse + ensure + Excon.stubs.shift + end + expect(api.get_apps.body.map { |app| app["name"] }).to eq( + %w( example example-fork ) + ) + end + + it "deletes fork app on error, before re-raising" do + stub(Heroku::Command).confirm_command.returns(true) + expect(api.get_apps.body.map { |app| app["name"] }).to eq(%w( example )) + end + end + end +end From 5552e6e0f93599a49a0d6e053d37fb89c2ef3506 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 21 Mar 2015 12:15:45 -0700 Subject: [PATCH 401/952] v3.30.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 56468f936..4a070d3a3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.30.3 2015-03-21 +================= +Reverted v4 fork implmentation + 3.30.2 2015-03-19 ================= Made updater also update v4 if it is setup diff --git a/Gemfile.lock b/Gemfile.lock index 25dd47ba0..a7a1337f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.2) + heroku (3.30.3) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index b9641257c..1f750fc93 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.2" + VERSION = "3.30.3" end From 5d06cb4ccc20bf12136621765ce71a6f152faba3 Mon Sep 17 00:00:00 2001 From: Troels Thomsen Date: Mon, 23 Mar 2015 11:11:54 +0100 Subject: [PATCH 402/952] Fix example output --- lib/heroku/command/stack.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/stack.rb b/lib/heroku/command/stack.rb index 25f7bbda5..7036590d0 100644 --- a/lib/heroku/command/stack.rb +++ b/lib/heroku/command/stack.rb @@ -13,7 +13,7 @@ class Stack < Base # # $ heroku stack # === example Available Stacks - # cedar + # cedar-10 # * cedar-14 # def index From a0d0ddcdcedc28fa2106483adb70b3f74dd42398 Mon Sep 17 00:00:00 2001 From: Jason Berlinsky Date: Mon, 23 Mar 2015 12:59:14 -0400 Subject: [PATCH 403/952] Bump rest-client dependency --- Gemfile.lock | 32 +++++++++++++++++++------------- heroku.gemspec | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a7a1337f0..54b0400f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,7 @@ PATH launchy (>= 0.3.2) multi_json (~> 1.10) netrc (>= 0.10.0) - rest-client (= 1.6.7) + rest-client (>= 1.7.3) rubyzip (= 0.9.9) GEM @@ -18,18 +18,19 @@ GEM mime-types xml-simple builder (3.2.2) - coveralls (0.7.2) - multi_json (~> 1.3) - rest-client (= 1.6.7) - simplecov (>= 0.7) - term-ansicolor (= 1.2.2) - thor (= 0.18.1) + coveralls (0.7.11) + multi_json (~> 1.10) + rest-client (>= 1.6.8, < 2) + simplecov (~> 0.9.1) + term-ansicolor (~> 1.3) + thor (~> 0.19.1) crack (0.4.2) safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) excon (0.44.4) fakefs (0.5.4) + ffi (1.9.8-x86-mingw32) heroku-api (0.3.23) excon (~> 0.44) multi_json (~> 1.8) @@ -40,8 +41,13 @@ GEM multi_json (1.11.0) netrc (0.10.3) rake (10.4.2) - rest-client (1.6.7) - mime-types (>= 1.16) + rest-client (1.7.3) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + rest-client (1.7.3-x86-mingw32) + ffi (~> 1.9) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rr (1.1.2) rspec (3.2.0) rspec-core (~> 3.2.0) @@ -63,10 +69,10 @@ GEM multi_json (~> 1.0) simplecov-html (~> 0.9.0) simplecov-html (0.9.0) - term-ansicolor (1.2.2) - tins (~> 0.8) - thor (0.18.1) - tins (0.13.2) + term-ansicolor (1.3.0) + tins (~> 1.0) + thor (0.19.1) + tins (1.3.5) webmock (1.20.4) addressable (>= 2.3.6) crack (>= 0.3.2) diff --git a/heroku.gemspec b/heroku.gemspec index 95134a271..9298af3b6 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |gem| gem.add_dependency "heroku-api", "~> 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", ">= 0.10.0" - gem.add_dependency "rest-client", "= 1.6.7" + gem.add_dependency "rest-client", ">= 1.7.3" gem.add_dependency "rubyzip", "= 0.9.9" gem.add_dependency "multi_json", "~> 1.10" end From c1c3c5c4a3b5aef97327fb85adf264f272d50a6e Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 26 Mar 2015 09:29:23 -0700 Subject: [PATCH 404/952] Better pg:backups public-url handling Check for .tty? like pgbackups:url instead of printing to stderr. This required some minor changes to the execute helper. --- lib/heroku/command/pg_backups.rb | 10 ++++++++-- spec/heroku/command/pg_backups_spec.rb | 12 +++++++++--- spec/spec_helper.rb | 12 +++++++----- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index d711d26f6..754501a0c 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -397,8 +397,14 @@ def public_url end url_info = client.transfers_public_url(backup_num) - $stderr.puts "The following URL will expire at #{url_info[:expires_at]}:" - display url_info[:url] + if $stdout.tty? + display <<-EOF +The following URL will expire at #{url_info[:expires_at]}: + "#{url_info[:url]}" +EOF + else + display url_info[:url] + end end def cancel_backup diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 0f827d233..2a2a4d11c 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -233,14 +233,20 @@ module Heroku::Command it "gets a public url for the specified backup" do stderr, stdout = execute("pg:backups public-url b001") + expect(stdout).to include url1_info[:url] + expect(stdout).to match(/will expire at #{Regexp.quote(url1_info[:expires_at].to_s)}/) + end + + it "only prints the url if stdout is not a tty" do + fake_stdout = StringIO.new + stderr, stdout = execute("pg:backups public-url b001", { stdout: fake_stdout }) expect(stdout.chomp).to eq url1_info[:url] - expect(stderr).to match(/will expire at #{Regexp.quote(url1_info[:expires_at].to_s)}/) end it "defaults to the latest backup if none is specified" do stderr, stdout = execute("pg:backups public-url") - expect(stdout.chomp).to eq url2_info[:url] - expect(stderr).to match(/will expire at #{Regexp.quote(url2_info[:expires_at].to_s)}/) + expect(stdout).to include url2_info[:url] + expect(stdout).to match(/will expire at #{Regexp.quote(url2_info[:expires_at].to_s)}/) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fba558d9f..adba71126 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -42,7 +42,7 @@ def prepare_command(klass) command end -def execute(command_line) +def execute(command_line, opts={}) extend RR::Adapters::RRMethods args = command_line.split(" ") @@ -60,15 +60,17 @@ def execute(command_line) original_stdin, original_stderr, original_stdout = $stdin, $stderr, $stdout - $stdin = captured_stdin = StringIO.new - $stderr = captured_stderr = StringIO.new - $stdout = captured_stdout = StringIO.new - class << captured_stdout + fake_tty_stdout = StringIO.new + class << fake_tty_stdout def tty? true end end + $stdin = captured_stdin = opts.fetch(:stdin, StringIO.new) + $stderr = captured_stderr = opts.fetch(:stderr, StringIO.new) + $stdout = captured_stdout = opts.fetch(:stdout, fake_tty_stdout) + begin object.send(method) rescue SystemExit From 96e78ecf5ceec1f8e8fc027df3c5c659faf6db74 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 26 Mar 2015 09:49:58 -0700 Subject: [PATCH 405/952] Fix specs for 1.8.7 --- spec/heroku/command/pg_backups_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 2a2a4d11c..f9d73b59d 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -239,7 +239,7 @@ module Heroku::Command it "only prints the url if stdout is not a tty" do fake_stdout = StringIO.new - stderr, stdout = execute("pg:backups public-url b001", { stdout: fake_stdout }) + stderr, stdout = execute("pg:backups public-url b001", { :stdout => fake_stdout }) expect(stdout.chomp).to eq url1_info[:url] end From 2db44e036af8a6ef28b33bdea15bed349f49c50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Ondruch?= Date: Tue, 31 Mar 2015 14:05:24 +0200 Subject: [PATCH 406/952] Properly include LICENSE file in gem This was bronek by 4533d240dd731a82146bdfc1f9284d414814cbe6 --- heroku.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heroku.gemspec b/heroku.gemspec index 95134a271..b45f8f3ce 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |gem| ! For API access, see: https://github.com/heroku/heroku.rb MESSAGE - gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(License|README|bin/|data/|ext/|lib/|spec/|test/)} } + gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(LICENSE|README|bin/|data/|ext/|lib/|spec/|test/)} } gem.add_dependency "heroku-api", "~> 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" From 63fa2977f82588c175ebcf637de3c71fd40892a0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 1 Apr 2015 16:49:38 -0700 Subject: [PATCH 407/952] v3.30.4 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4a070d3a3..832584aba 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.30.4 2015-04-01 +================= +Postgres backups cleanup and fixes + 3.30.3 2015-03-21 ================= Reverted v4 fork implmentation diff --git a/Gemfile.lock b/Gemfile.lock index a7a1337f0..4afe84827 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.3) + heroku (3.30.4) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1f750fc93..305484a99 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.3" + VERSION = "3.30.4" end From fded45b7f25089bbe108c0da07cf40001cab353c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Apr 2015 11:03:07 -0700 Subject: [PATCH 408/952] added warning for users on ruby 1.8.7 --- lib/heroku/cli.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index eb44b6a02..79593a89e 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,3 +1,8 @@ +if RUBY_VERSION < '1.9.0' + $stderr.puts "WARNING: Heroku Toolbelt will require Ruby 1.9+ beginning next week" + $stderr.puts "https://github.com/heroku/heroku/pull/1479" +end + load('heroku/helpers.rb') # reload helpers after possible inject_loadpath load('heroku/updater.rb') # reload updater after possible inject_loadpath From 042b8552d8ac61c4415ccde26ce47f33ac2b744e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Apr 2015 11:05:39 -0700 Subject: [PATCH 409/952] v3.30.5 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 832584aba..bcb57b5c7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.30.5 2015-04-02 +================= +Added warning for ruby 1.8.7 + 3.30.4 2015-04-01 ================= Postgres backups cleanup and fixes diff --git a/Gemfile.lock b/Gemfile.lock index 4afe84827..00fa3d60e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.4) + heroku (3.30.5) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 305484a99..30cabc934 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.4" + VERSION = "3.30.5" end From 7f650927d4255e99519be3c65d085b66ecf4a8ce Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Apr 2015 16:04:10 -0700 Subject: [PATCH 410/952] do not perform stty icanon echo on windows --- lib/heroku/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 79593a89e..863b6fcbe 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -47,7 +47,7 @@ def self.start(*args) rescue Errno::EPIPE => e error(e.message) rescue Interrupt => e - `stty icanon echo` + `stty icanon echo` unless running_on_windows? if ENV["HEROKU_DEBUG"] styled_error(e) else From 9d70342ff6e53d4513b49f88e55c2a931ddbcc8f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Apr 2015 16:13:50 -0700 Subject: [PATCH 411/952] removed date from ruby 1.9 warning --- lib/heroku/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 863b6fcbe..2ef7b313a 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,5 +1,5 @@ if RUBY_VERSION < '1.9.0' - $stderr.puts "WARNING: Heroku Toolbelt will require Ruby 1.9+ beginning next week" + $stderr.puts "WARNING: Heroku Toolbelt requires Ruby 1.9+" $stderr.puts "https://github.com/heroku/heroku/pull/1479" end From 7321be09674585707759231063df78bede2990ae Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 31 Mar 2015 17:34:54 -0700 Subject: [PATCH 412/952] removed support for ruby 1.8.7 --- .travis.yml | 1 - heroku.gemspec | 1 + lib/heroku/cli.rb | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1127b0f00..ee73af1a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: ruby rvm: - - 1.8.7 - 1.9.3 - 2.0.0 - 2.1.5 diff --git a/heroku.gemspec b/heroku.gemspec index b45f8f3ce..c22f38e60 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -12,6 +12,7 @@ Gem::Specification.new do |gem| gem.description = "Client library and command-line tool to deploy and manage apps on Heroku." gem.executables = "heroku" gem.license = "MIT" + gem.required_ruby_version = ">= 1.9.0" gem.post_install_message = <<-MESSAGE ! The `heroku` gem has been deprecated and replaced with the Heroku Toolbelt. ! Download and install from: https://toolbelt.heroku.com diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 2ef7b313a..c5a523526 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -1,6 +1,6 @@ -if RUBY_VERSION < '1.9.0' - $stderr.puts "WARNING: Heroku Toolbelt requires Ruby 1.9+" - $stderr.puts "https://github.com/heroku/heroku/pull/1479" +if RUBY_VERSION < '1.9.0' # this is a string comparison, but it should work for any old ruby version + $stderr.puts "Heroku Toolbelt requires Ruby 1.9+." + exit 1 end load('heroku/helpers.rb') # reload helpers after possible inject_loadpath From 1c67e6a2cbe33dde13dd3cb16b5f585469a27ac3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Apr 2015 16:16:20 -0700 Subject: [PATCH 413/952] v3.30.6 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bcb57b5c7..bd53ddfa6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.30.6 2015-04-02 +================= +Skipped calling of `stty icanon echo` on Windows +Updated 1.8.7 warning + 3.30.5 2015-04-02 ================= Added warning for ruby 1.8.7 diff --git a/Gemfile.lock b/Gemfile.lock index 00fa3d60e..acff733f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.5) + heroku (3.30.6) heroku-api (~> 0.3.19) launchy (>= 0.3.2) multi_json (~> 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 30cabc934..b70950ae5 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.5" + VERSION = "3.30.6" end From e396af323657a5c41ce0ab533317c9a3d6076749 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 2 Apr 2015 16:39:04 -0700 Subject: [PATCH 414/952] hide run:console --- lib/heroku/command/run.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index ab0b0d45b..868066b0b 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -98,7 +98,7 @@ def rake alias_command "rake", "run:rake" - # run:console [COMMAND] + # HIDDEN: run:console [COMMAND] # # open a remote console session # From 27e0ab7b71a2b34ac9713bd85cd43dbd685cdaf3 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 2 Apr 2015 21:53:59 -0700 Subject: [PATCH 415/952] Show old pgbackups transfer names --- lib/heroku/command/pg_backups.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 754501a0c..804888d52 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -144,8 +144,13 @@ def list_backups display_backups = transfers.select do |b| b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' end.sort_by { |b| b[:created_at] }.reverse.map do |b| + old_pgb_name = b.has_key?(:options) && b[:options]["pgbackups_name"] + id = transfer_name(b[:num]) + if old_pgb_name + id += " (was #{old_pgb_name})" + end { - "id" => transfer_name(b[:num]), + "id" => id, "created_at" => b[:created_at], "status" => transfer_status(b), "size" => size_pretty(b[:processed_bytes]), From 74e0821b8e276e29acb291eb173c9b9b0e520bd1 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 7 Apr 2015 15:19:57 -0700 Subject: [PATCH 416/952] Better transfer_name function; use it more consistently --- lib/heroku/command/pg_backups.rb | 57 +++++++++++++++++++------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 804888d52..6c4f7632e 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -91,8 +91,25 @@ def arbitrary_app_db generate_resolver.all_databases.values.first end - def transfer_name(backup_num, prefix='b') - "#{prefix}#{format("%03d", backup_num)}" + def transfer_name(transfer) + old_pgb_name = transfer.has_key?(:options) && transfer[:options]["pgbackups_name"] + + if old_pgb_name + "o#{old_pgb_name}" + else + transfer_num = transfer[:num] + from_type, to_type = transfer[:from_type], transfer[:to_type] + prefix = if from_type == 'pg_dump' && to_type != 'pg_restore' + transfer.has_key?(:schedule) ? 'a' : 'b' + elsif from_type != 'pg_dump' && to_type == 'pg_restore' + 'r' + elsif from_type == 'pg_dump' && to_type == 'pg_restore' + 'c' + else + 'b' + end + "#{prefix}#{format("%03d", transfer_num)}" + end end def backup_num(transfer_name) @@ -144,13 +161,8 @@ def list_backups display_backups = transfers.select do |b| b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' end.sort_by { |b| b[:created_at] }.reverse.map do |b| - old_pgb_name = b.has_key?(:options) && b[:options]["pgbackups_name"] - id = transfer_name(b[:num]) - if old_pgb_name - id += " (was #{old_pgb_name})" - end { - "id" => id, + "id" => transfer_name(b), "created_at" => b[:created_at], "status" => transfer_status(b), "size" => size_pretty(b[:processed_bytes]), @@ -172,7 +184,7 @@ def list_backups r[:from_type] == 'gof3r' && r[:to_type] == 'pg_restore' end.sort_by { |r| r[:created_at] }.reverse.map do |r| { - "id" => transfer_name(r[:num], 'r'), + "id" => transfer_name(r), "created_at" => r[:created_at], "status" => transfer_status(r), "size" => size_pretty(r[:processed_bytes]), @@ -191,12 +203,12 @@ def list_backups end def backup_status - backup_id = shift_argument + backup_name = shift_argument validate_arguments! verbose = true client = hpg_app_client(app) - backup = if backup_id.nil? + backup = if backup_name.nil? backups = client.transfers last_backup = backups.select do |b| b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' @@ -204,7 +216,6 @@ def backup_status if last_backup.nil? error("No backups. Capture one with `heroku pg:backups capture`.") else - backup_id = transfer_name(last_backup[:num]) if verbose client.transfers_get(last_backup[:num], verbose) else @@ -212,7 +223,7 @@ def backup_status end end else - client.transfers_get(backup_num(backup_id), verbose) + client.transfers_get(backup_num(backup_name), verbose) end status = if backup[:succeeded] "Completed Successfully" @@ -234,8 +245,9 @@ def backup_status orig_size = backup[:source_bytes] || backup_size compression_pct = [((orig_size - backup_size).to_f / orig_size * 100).round, 0].max + backup_name = transfer_name(backup) display <<-EOF -=== Backup info: #{backup_id} +=== Backup info: #{backup_name} Database: #{backup[:from_name]} EOF if backup[:started_at] @@ -281,7 +293,7 @@ def capture_backup will continue running. Use heroku pg:backups info to check progress. Stop a running backup with heroku pg:backups cancel. -#{attachment.name} ---backup---> #{transfer_name(backup[:num])} +#{attachment.name} ---backup---> #{transfer_name(backup)} EOF poll_transfer('backup', backup[:uuid]) @@ -308,22 +320,21 @@ def restore_backup restore_url = restore_from else # assume we're restoring from a backup - backup_id = restore_from + backup_name = restore_from backups = hpg_app_client(app).transfers.select do |b| b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' end - backup = if backup_id == :latest - # N.B.: this also handles the empty backups case + backup = if backup_name == :latest backups.sort_by { |b| b[:started_at] }.last else - backups.find { |b| transfer_name(b[:num]) == backup_id } + backups.find { |b| transfer_name(b) == backup_name } end if backups.empty? abort("No backups. Capture one with `heroku pg:backups capture`.") elsif backup.nil? - abort("Backup #{backup_id} not found.") + abort("Backup #{backup_name} not found.") elsif !backup[:succeeded] - abort("Backup #{backup_id} did not complete successfully; cannot restore it.") + abort("Backup #{backup_name} did not complete successfully; cannot restore it.") end restore_url = backup[:to_url] end @@ -335,7 +346,7 @@ def restore_backup will continue restoring. Use heroku pg:backups to check progress. Stop a running restore with heroku pg:backups cancel. -#{transfer_name(restore[:num])} ---restore---> #{attachment.name} +#{transfer_name(restore)} ---restore---> #{attachment.name} EOF poll_transfer('restore', restore[:uuid]) @@ -418,7 +429,7 @@ def cancel_backup client = hpg_app_client(app) transfer = client.transfers.find { |b| b[:finished_at].nil? } client.transfers_cancel(transfer[:uuid]) - display "Canceled #{transfer_name(transfer[:num])}" + display "Canceled #{transfer_name(transfer)}" end def schedule_backups From 5c8a38a450a8d72a7dd6cfb3eb2aa025e0254b49 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 7 Apr 2015 18:13:49 -0700 Subject: [PATCH 417/952] Cleaner support for PGBackups backups --- lib/heroku/command/pg_backups.rb | 36 +++++++++++++++++++------- spec/heroku/command/pg_backups_spec.rb | 36 +++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 6c4f7632e..034d97c4c 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -112,8 +112,15 @@ def transfer_name(transfer) end end - def backup_num(transfer_name) - /b(\d+)/.match(transfer_name) && $1.to_i + def transfer_num(transfer_name) + if /\A[abcr](\d+)\z/.match(transfer_name) + $1.to_i + elsif /\Ao[ab]\d+\z/.match(transfer_name) + xfer = hpg_app_client(app).transfers.find do |t| + transfer_name(t) == transfer_name + end + xfer[:num] unless xfer.nil? + end end def transfer_status(t) @@ -223,7 +230,11 @@ def backup_status end end else - client.transfers_get(backup_num(backup_name), verbose) + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup: #{backup_num}") + end + client.transfers_get(backup_num, verbose) end status = if backup[:succeeded] "Completed Successfully" @@ -384,23 +395,30 @@ def poll_transfer(action, transfer_id) end def delete_backup - backup_id = shift_argument + backup_name = shift_argument validate_arguments! if confirm_command - hpg_app_client(app).transfers_delete(backup_num(backup_id)) - display "Deleted #{backup_id}" + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup: #{backup_num}") + end + hpg_app_client(app).transfers_delete(backup_num) + display "Deleted #{backup_name}" end end def public_url - backup_id = shift_argument + backup_name = shift_argument validate_arguments! backup_num = nil client = hpg_app_client(app) - if backup_id - backup_num = backup_num(backup_id) + if backup_name + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup: #{backup_num}") + end else last_successful_backup = client.transfers.select do |xfer| xfer[:succeeded] && xfer[:to_type] == 'gof3r' diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index f9d73b59d..870c6c9df 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -140,9 +140,17 @@ module Heroku::Command :started_at => started_at, :finished_at => finished_at, :processed_bytes => backup_size, :source_bytes => source_size, :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', + :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', + :num => 2, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :options => { "pgbackups_name" => "b047" }, + :succeeded => true }, { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', :from_name => from_name, :to_name => 'BACKUP', - :num => 2, :logs => logs, + :num => 3, :logs => logs, :from_type => 'pg_dump', :to_type => 'gof3r', :started_at => started_at, :finished_at => finished_at, :processed_bytes => backup_size, :source_bytes => source_size, @@ -151,8 +159,10 @@ module Heroku::Command end before do - stub_pgapp.transfers_get(2, true).returns(transfers.find { |xfer| xfer[:num] == 2 }) - stub_pgapp.transfers_get(1, true).returns(transfers.find { |xfer| xfer[:num] == 1 }) + (1..3).each do |n| + stub_pgapp.transfers_get(n, true) + .returns(transfers.find { |xfer| xfer[:num] == n }) + end stub_pgapp.transfers.returns(transfers) end @@ -173,11 +183,28 @@ module Heroku::Command EOF end + it "displays info for legacy PGBackups backups" do + stderr, stdout = execute("pg:backups info ob047") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: ob047 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B (50% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + it "defaults to the latest backup if none is specified" do stderr, stdout = execute("pg:backups info") expect(stderr).to be_empty expect(stdout).to eq <<-EOF -=== Backup info: b002 +=== Backup info: b003 Database: #{from_name} Started: #{started_at} Finished: #{finished_at} @@ -249,5 +276,6 @@ module Heroku::Command expect(stdout).to match(/will expire at #{Regexp.quote(url2_info[:expires_at].to_s)}/) end end + end end From 99dd9d0f8949e2c644a0da69f96e8a48d76d9c9a Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Wed, 8 Apr 2015 10:50:34 -0700 Subject: [PATCH 418/952] Fix specs for 1.8.7 --- spec/heroku/command/pg_backups_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 870c6c9df..f354460bd 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -160,8 +160,8 @@ module Heroku::Command before do (1..3).each do |n| - stub_pgapp.transfers_get(n, true) - .returns(transfers.find { |xfer| xfer[:num] == n }) + stub_pgapp.transfers_get(n, true). + returns(transfers.find { |xfer| xfer[:num] == n }) end stub_pgapp.transfers.returns(transfers) end From 216e60b16d6139aeea4277c2d15a6e09f7bb97fb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 8 Apr 2015 16:51:00 -0700 Subject: [PATCH 419/952] updated gems --- Gemfile | 4 ++-- Gemfile.lock | 42 ++++++++++++++++++++++++++---------------- heroku.gemspec | 6 +++--- lib/heroku/updater.rb | 6 +++--- tasks/zip.rake | 4 ++-- 5 files changed, 36 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index b7dbe2580..2125d3971 100644 --- a/Gemfile +++ b/Gemfile @@ -6,8 +6,8 @@ group :development, :test do gem "rake" gem "rr" gem "aws-s3" - gem "mime-types", "< 2.0" - gem "fakefs", "< 0.6" + gem "mime-types" + gem "fakefs" gem "json" gem "rspec" gem "webmock" diff --git a/Gemfile.lock b/Gemfile.lock index d11baf5e1..356083e26 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,23 +2,23 @@ PATH remote: . specs: heroku (3.30.6) - heroku-api (~> 0.3.19) + heroku-api (>= 0.3.19) launchy (>= 0.3.2) - multi_json (~> 1.10) + multi_json (>= 1.10) netrc (>= 0.10.0) rest-client (>= 1.7.3) - rubyzip (= 0.9.9) + rubyzip (>= 0.9.9) GEM remote: https://rubygems.org/ specs: - addressable (2.3.7) + addressable (2.3.8) aws-s3 (0.6.3) builder mime-types xml-simple builder (3.2.2) - coveralls (0.7.11) + coveralls (0.8.0) multi_json (~> 1.10) rest-client (>= 1.6.8, < 2) simplecov (~> 0.9.1) @@ -28,24 +28,30 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.44.4) - fakefs (0.5.4) + domain_name (0.5.23) + unf (>= 0.0.5, < 1.0.0) + excon (0.45.1) + fakefs (0.6.7) ffi (1.9.8-x86-mingw32) heroku-api (0.3.23) excon (~> 0.44) multi_json (~> 1.8) + http-cookie (1.0.2) + domain_name (~> 0.5) json (1.8.2) launchy (2.4.3) addressable (~> 2.3) - mime-types (1.25.1) + mime-types (2.4.3) multi_json (1.11.0) netrc (0.10.3) rake (10.4.2) - rest-client (1.7.3) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) - rest-client (1.7.3-x86-mingw32) + rest-client (1.8.0-x86-mingw32) ffi (~> 1.9) + http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) rr (1.1.2) @@ -53,16 +59,16 @@ GEM rspec-core (~> 3.2.0) rspec-expectations (~> 3.2.0) rspec-mocks (~> 3.2.0) - rspec-core (3.2.2) + rspec-core (3.2.3) rspec-support (~> 3.2.0) - rspec-expectations (3.2.0) + rspec-expectations (3.2.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-mocks (3.2.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.2.0) rspec-support (3.2.2) - rubyzip (0.9.9) + rubyzip (1.1.7) safe_yaml (1.0.4) simplecov (0.9.2) docile (~> 1.1.0) @@ -73,7 +79,11 @@ GEM tins (~> 1.0) thor (0.19.1) tins (1.3.5) - webmock (1.20.4) + unf (0.1.4) + unf_ext + unf_ext (0.0.6) + unf_ext (0.0.6-x86-mingw32) + webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) xml-simple (1.1.5) @@ -85,10 +95,10 @@ PLATFORMS DEPENDENCIES aws-s3 coveralls - fakefs (< 0.6) + fakefs heroku! json - mime-types (< 2.0) + mime-types rake rr rspec diff --git a/heroku.gemspec b/heroku.gemspec index db1c3fb69..6cb846716 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -21,10 +21,10 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(LICENSE|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", "~> 0.3.19" + gem.add_dependency "heroku-api", ">= 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", ">= 0.10.0" gem.add_dependency "rest-client", ">= 1.7.3" - gem.add_dependency "rubyzip", "= 0.9.9" - gem.add_dependency "multi_json", "~> 1.10" + gem.add_dependency "rubyzip", ">= 0.9.9" + gem.add_dependency "multi_json", ">= 1.10" end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 15a14cb5d..ea161b6cf 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -107,7 +107,7 @@ def self.update(prerelease=false) stderr_print 'updating...' wait_for_lock do require "tmpdir" - require "zip/zip" + require "zip" Dir.mktmpdir do |download_dir| zip_filename = "#{download_dir}/heroku.zip" @@ -148,11 +148,11 @@ def self.download_file(from_url, to_filename) end def self.extract_zip(filename, dir) - Zip::ZipFile.open(filename) do |zip| + Zip::File.open(filename) do |zip| zip.each do |entry| target = File.join(dir, entry.to_s) FileUtils.mkdir_p File.dirname(target) - zip.extract(entry, target) { true } + entry.extract(target) { true } end end end diff --git a/tasks/zip.rake b/tasks/zip.rake index 194abc78d..82c7a6c39 100644 --- a/tasks/zip.rake +++ b/tasks/zip.rake @@ -1,4 +1,4 @@ -require "zip/zip" +require "zip" namespace :zip do desc "build zip" @@ -22,7 +22,7 @@ namespace :zip do cd "heroku-client" do assemble_distribution assemble_gems - Zip::ZipFile.open(t.name, Zip::ZipFile::CREATE) do |zip| + Zip::File.open(t.name, Zip::File::CREATE) do |zip| Dir["**/*"].each do |file| zip.add(file, file) { true } end From be26294886bf0667ec3b3c0b67e8d62eccb5d818 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 8 Apr 2015 17:08:50 -0700 Subject: [PATCH 420/952] v3.31.0 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bd53ddfa6..e482ac72f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.31.0 2015-04-08 +================= +Removed support for Ruby 1.8.7 +Updated all dependencies to latest +Show backups that will be migrated from PGBackups + 3.30.6 2015-04-02 ================= Skipped calling of `stty icanon echo` on Windows diff --git a/Gemfile.lock b/Gemfile.lock index 356083e26..b87d060f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.30.6) + heroku (3.31.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index b70950ae5..4fa9388f7 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.30.6" + VERSION = "3.31.0" end From aca7212f42d36de351b35ffc3bcce4a020f71e44 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 8 Apr 2015 17:28:27 -0700 Subject: [PATCH 421/952] removed windows-specific gems from Gemfile.lock --- Gemfile.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b87d060f2..20746c9f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -32,7 +32,6 @@ GEM unf (>= 0.0.5, < 1.0.0) excon (0.45.1) fakefs (0.6.7) - ffi (1.9.8-x86-mingw32) heroku-api (0.3.23) excon (~> 0.44) multi_json (~> 1.8) @@ -49,11 +48,6 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) - rest-client (1.8.0-x86-mingw32) - ffi (~> 1.9) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) rr (1.1.2) rspec (3.2.0) rspec-core (~> 3.2.0) @@ -82,7 +76,6 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.6) - unf_ext (0.0.6-x86-mingw32) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) @@ -90,7 +83,6 @@ GEM PLATFORMS ruby - x86-mingw32 DEPENDENCIES aws-s3 From d2b1c4734a00d9477e57cfacb523c07d150a0072 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 8 Apr 2015 17:30:00 -0700 Subject: [PATCH 422/952] v3.31.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e482ac72f..8e99f32d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.31.1 2015-04-08 +================= +Removed windows-specific gems from Gemfile.lock + 3.31.0 2015-04-08 ================= Removed support for Ruby 1.8.7 diff --git a/Gemfile.lock b/Gemfile.lock index 20746c9f8..ce32eca92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.31.0) + heroku (3.31.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4fa9388f7..a7c0606db 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.31.0" + VERSION = "3.31.1" end From 70954a83ac6f1801fee6afbd8a9a97a44d17573c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 8 Apr 2015 17:42:43 -0700 Subject: [PATCH 423/952] downgraded rest-client to 1.6.8 to fix issue with ffi on windows --- Gemfile.lock | 19 ++++++------------- heroku.gemspec | 2 +- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ce32eca92..74e5b9c23 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,7 +6,7 @@ PATH launchy (>= 0.3.2) multi_json (>= 1.10) netrc (>= 0.10.0) - rest-client (>= 1.7.3) + rest-client (>= 1.6.0) rubyzip (>= 0.9.9) GEM @@ -28,26 +28,22 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - domain_name (0.5.23) - unf (>= 0.0.5, < 1.0.0) excon (0.45.1) fakefs (0.6.7) heroku-api (0.3.23) excon (~> 0.44) multi_json (~> 1.8) - http-cookie (1.0.2) - domain_name (~> 0.5) json (1.8.2) launchy (2.4.3) addressable (~> 2.3) - mime-types (2.4.3) + mime-types (1.25.1) multi_json (1.11.0) netrc (0.10.3) rake (10.4.2) - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) + rdoc (4.2.0) + rest-client (1.6.8) + mime-types (~> 1.16) + rdoc (>= 2.4.2) rr (1.1.2) rspec (3.2.0) rspec-core (~> 3.2.0) @@ -73,9 +69,6 @@ GEM tins (~> 1.0) thor (0.19.1) tins (1.3.5) - unf (0.1.4) - unf_ext - unf_ext (0.0.6) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) diff --git a/heroku.gemspec b/heroku.gemspec index 6cb846716..c5a258f6a 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |gem| gem.add_dependency "heroku-api", ">= 0.3.19" gem.add_dependency "launchy", ">= 0.3.2" gem.add_dependency "netrc", ">= 0.10.0" - gem.add_dependency "rest-client", ">= 1.7.3" + gem.add_dependency "rest-client", ">= 1.6.0" gem.add_dependency "rubyzip", ">= 0.9.9" gem.add_dependency "multi_json", ">= 1.10" end From ff2d3fdb84b7939845311313d51933a2d746178a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 8 Apr 2015 17:46:47 -0700 Subject: [PATCH 424/952] v3.31.2 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8e99f32d1..9e32f9e57 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.31.2 2015-04-08 +================= +Downgraded bundled version of rest-client to one that does not require ffi on windows + 3.31.1 2015-04-08 ================= Removed windows-specific gems from Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock index 74e5b9c23..16cc7c0bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.31.1) + heroku (3.31.2) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index a7c0606db..fa34ea709 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.31.1" + VERSION = "3.31.2" end From c14f8c823f1b2d6b11355b75429ca108ff9c1dee Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Thu, 9 Apr 2015 15:59:22 +0000 Subject: [PATCH 425/952] added an option to force suppression of pg:backups public-url expiration --- lib/heroku/command/pg_backups.rb | 3 ++- spec/heroku/command/pg_backups_spec.rb | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 034d97c4c..09c0523f0 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -39,6 +39,7 @@ def copy # capture DATABASE # capture a new backup # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) # public-url BACKUP_ID # get secret but publicly accessible URL for BACKUP_ID to download it + # -s, --suppress # Suppress expiration message (for use in scripts) # cancel # cancel an in-progress backup # delete BACKUP_ID # delete an existing backup # schedule DATABASE # schedule nightly backups for given database @@ -431,7 +432,7 @@ def public_url end url_info = client.transfers_public_url(backup_num) - if $stdout.tty? + if $stdout.tty? && !options[:suppress] display <<-EOF The following URL will expire at #{url_info[:expires_at]}: "#{url_info[:url]}" diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index f354460bd..28396b0e4 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -270,6 +270,11 @@ module Heroku::Command expect(stdout.chomp).to eq url1_info[:url] end + it "only prints the url if called with -s" do + stderr, stdout = execute("pg:backups public-url b001 -s") + expect(stdout.chomp).to eq url1_info[:url] + end + it "defaults to the latest backup if none is specified" do stderr, stdout = execute("pg:backups public-url") expect(stdout).to include url2_info[:url] From 97f48d0f734bb737cac7fb03f60a7b63ee997b08 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 9 Apr 2015 10:07:50 -0700 Subject: [PATCH 426/952] Safeguard against edge case sizes --- lib/heroku/command/pg_backups.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 034d97c4c..639d5731e 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -255,7 +255,11 @@ def backup_status backup_size = backup[:processed_bytes] orig_size = backup[:source_bytes] || backup_size - compression_pct = [((orig_size - backup_size).to_f / orig_size * 100).round, 0].max + compression_pct = if backup_size > 0 && orig_size > 0 + [((orig_size - backup_size).to_f / orig_size * 100).round, 0].max + else + 0 + end backup_name = transfer_name(backup) display <<-EOF === Backup info: #{backup_name} From 44e7bd244eacf5bf00466e99b913a187c217733d Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Thu, 9 Apr 2015 19:18:57 +0000 Subject: [PATCH 427/952] changed to quiet --- lib/heroku/command/pg_backups.rb | 4 ++-- spec/heroku/command/pg_backups_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 09c0523f0..8d8a98001 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -39,7 +39,7 @@ def copy # capture DATABASE # capture a new backup # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) # public-url BACKUP_ID # get secret but publicly accessible URL for BACKUP_ID to download it - # -s, --suppress # Suppress expiration message (for use in scripts) + # -q, --quiet # Hide expiration message (for use in scripts) # cancel # cancel an in-progress backup # delete BACKUP_ID # delete an existing backup # schedule DATABASE # schedule nightly backups for given database @@ -432,7 +432,7 @@ def public_url end url_info = client.transfers_public_url(backup_num) - if $stdout.tty? && !options[:suppress] + if $stdout.tty? && !options[:quiet] display <<-EOF The following URL will expire at #{url_info[:expires_at]}: "#{url_info[:url]}" diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 28396b0e4..b9ed2479c 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -270,8 +270,8 @@ module Heroku::Command expect(stdout.chomp).to eq url1_info[:url] end - it "only prints the url if called with -s" do - stderr, stdout = execute("pg:backups public-url b001 -s") + it "only prints the url if called with -q" do + stderr, stdout = execute("pg:backups public-url b001 -q") expect(stdout.chomp).to eq url1_info[:url] end From 35b13de0ab15c220b67cd2035a196ccedf0c678a Mon Sep 17 00:00:00 2001 From: Henrik Hodne Date: Thu, 9 Apr 2015 21:57:16 +0000 Subject: [PATCH 428/952] Add FreeBSD as supported OS --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 9608561fb..39ec3816a 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -131,6 +131,8 @@ def self.os "windows" when /openbsd/ "openbsd" + when /freebsd/ + "freebsd" else raise "unsupported on #{RbConfig::CONFIG['host_os']}" end From 6330688a22df4ac1676b2bc32f0f5750b1682166 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 9 Apr 2015 15:02:02 -0700 Subject: [PATCH 429/952] Better heuristic for picking "latest" backup to restore --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 639d5731e..571a1a03b 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -340,7 +340,7 @@ def restore_backup b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' end backup = if backup_name == :latest - backups.sort_by { |b| b[:started_at] }.last + backups.select { |b| b[:succeeded] }.sort_by { |b| b[:num] }.last else backups.find { |b| transfer_name(b) == backup_name } end From 628fda9206427657f3fc260a3ae50475dd219c09 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 9 Apr 2015 15:02:42 -0700 Subject: [PATCH 430/952] use ssl for downloading v4 jsplugins --- lib/heroku/jsplugin.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 39ec3816a..17949a9dc 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -95,7 +95,8 @@ def self.setup return if File.exist? bin $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) - resp = Excon.get(url, :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + resp = Excon.get(url, opts) open(bin, "wb") do |file| file.write(resp.body) end @@ -139,7 +140,16 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("http://d1gvo455cekpjp.cloudfront.net/master/manifest.json").body) + @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/master/manifest.json", excon_opts).body) + end + + def self.excon_opts + if os == 'windows' + # S3 SSL downloads do not work from ruby in Windows + {:ssl_verify_peer => false} + else + {} + end end def self.url From 983319d89308b1da88abd7f87bbbfaffa19ca5ce Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 9 Apr 2015 16:21:14 -0700 Subject: [PATCH 431/952] Add spec; clean up backup status display --- lib/heroku/command/pg_backups.rb | 23 ++++++----- spec/heroku/command/pg_backups_spec.rb | 55 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 571a1a03b..40df6eed5 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -252,14 +252,7 @@ def backup_status else "Manual" end - backup_size = backup[:processed_bytes] - orig_size = backup[:source_bytes] || backup_size - compression_pct = if backup_size > 0 && orig_size > 0 - [((orig_size - backup_size).to_f / orig_size * 100).round, 0].max - else - 0 - end backup_name = transfer_name(backup) display <<-EOF === Backup info: #{backup_name} @@ -279,10 +272,22 @@ def backup_status Status: #{status} Type: #{type} EOF - if !orig_size.nil? && orig_size > 0 + backup_size = backup[:processed_bytes] + orig_size = backup[:source_bytes] || 0 + if orig_size > 0 + compress_str = "" + unless backup[:finished_at].nil? + compression_pct = if backup_size > 0 + [((orig_size - backup_size).to_f / orig_size * 100) + .round, 0].max + else + 0 + end + compress_str = " (#{compression_pct}% compression)" + end display <<-EOF Original DB Size: #{size_pretty(orig_size)} -Backup Size: #{size_pretty(backup_size)} (#{compression_pct}% compression) +Backup Size: #{size_pretty(backup_size)}#{compress_str} EOF else display <<-EOF diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index f354460bd..c3e0fcec2 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -213,6 +213,61 @@ module Heroku::Command Original DB Size: #{source_size}.0B Backup Size: #{backup_size}.0B (50% compression) === Backup Logs +#{logged_at}: hello world + EOF + end + + it "does not display finished time or compression ratio if backup is not finished" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:finished_at] = nil + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: #{backup_size}.0B +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "works when the progress is at 0 bytes" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:processed_bytes] = 0 + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Original DB Size: #{source_size}.0B +Backup Size: 0.00B (0% compression) +=== Backup Logs +#{logged_at}: hello world + EOF + end + + it "works when the source size is 0 bytes" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:source_bytes] = 0 + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: b001 +Database: #{from_name} +Started: #{started_at} +Finished: #{finished_at} +Status: Completed Successfully +Type: Manual +Backup Size: #{backup_size}.0B +=== Backup Logs #{logged_at}: hello world EOF end From 7622de4d19cb511327c1e2b133d7f756eea1c84e Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Thu, 9 Apr 2015 18:16:58 -0700 Subject: [PATCH 432/952] Default to restoring last successful backup and add specs --- lib/heroku/command/pg_backups.rb | 3 +- spec/heroku/command/pg_backups_spec.rb | 79 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 40df6eed5..1e60cb2bf 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -345,7 +345,8 @@ def restore_backup b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' end backup = if backup_name == :latest - backups.select { |b| b[:succeeded] }.sort_by { |b| b[:num] }.last + backups.select { |b| b[:succeeded] } + .sort_by { |b| b[:finished_at] }.last else backups.find { |b| transfer_name(b) == backup_name } end diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index c3e0fcec2..bb885293f 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -273,6 +273,85 @@ module Heroku::Command end end + describe "heroku pg:backups restore" do + let(:started_at) { Time.parse('2001-01-01 00:00:00') } + let(:finished_at_1) { Time.parse('2001-01-01 01:00:00') } + let(:finished_at_2) { Time.parse('2001-01-01 02:00:00') } + let(:finished_at_3) { Time.parse('2001-01-01 03:00:00') } + + let(:from_name) { 'RED' } + let(:to_url) { 'https://example.com/my-backup' } + + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', :num => 1, + :from_type => 'pg_dump', :to_type => 'gof3r', :to_url => to_url, + :started_at => started_at, :finished_at => finished_at_2, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', + :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', :num => 2, + :from_type => 'pg_dump', :to_type => 'gof3r', :to_url => to_url, + :started_at => started_at, :finished_at => finished_at_1, + :options => { "pgbackups_name" => "b047" }, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', + :from_name => from_name, :to_name => 'BACKUP', :num => 3, + :from_type => 'pg_dump', :to_type => 'gof3r', :to_url => to_url, + :started_at => started_at, :finished_at => finished_at_3, + :succeeded => false } + ] + end + + let(:restore_info) do + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 3, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true } + end + + before do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + stub_pgapp.transfers.returns(transfers) + end + + it "triggers a restore of the given backup" do + stub_pg.backups_restore(to_url).returns(restore_info) + stub_pgapp.transfers_get.returns(restore_info) + + stderr, stdout = execute("pg:backups restore b001 red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Restore completed/) + end + + it "defaults to the latest successful backup" do + stub_pg.backups_restore(to_url).returns(restore_info) + stub_pgapp.transfers_get.returns(restore_info) + + stderr, stdout = execute("pg:backups restore red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Restore completed/) + end + + it "refuses to restore a backup that did not complete successfully" do + stub_pg.backups_restore(to_url).returns(restore_info) + stub_pgapp.transfers_get.returns(restore_info) + + stderr, stdout = execute("pg:backups restore b003 red --confirm example") + expect(stderr).to match(/did not complete successfully/) + expect(stdout).to be_empty + end + + it "does not restore without confirmation" do + stderr, stdout = execute("pg:backups restore b001 red") + expect(stderr).to match(/Confirmation did not match example. Aborted./) + expect(stdout).to match(/WARNING: Destructive Action/) + expect(stdout).to match(/This command will affect the app: example/) + expect(stdout).to match(/To proceed, type "example" or re-run this command with --confirm example/) + end + end + describe "heroku pg:backups public-url" do let(:logged_at) { Time.now } let(:started_at) { Time.now } From 478dcd3698f9145fc35fef620065a129425befc0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 9 Apr 2015 18:40:27 -0700 Subject: [PATCH 433/952] v3.31.3 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9e32f9e57..cd5738a26 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.31.3 2015-04-09 +================= +Fixed some bugs around pg:backups +Fixed v4 download on windows +Add FreeBSD as supported os for v4 + 3.31.2 2015-04-08 ================= Downgraded bundled version of rest-client to one that does not require ffi on windows diff --git a/Gemfile.lock b/Gemfile.lock index 16cc7c0bf..667ea02fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.31.2) + heroku (3.31.3) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index fa34ea709..4aacf1c2f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.31.2" + VERSION = "3.31.3" end From c0abb0bb60117d40273432200d21ce9e2364c924 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 10 Apr 2015 15:17:48 -0700 Subject: [PATCH 434/952] workaround for ruby bug of home directory on windows --- lib/heroku/helpers.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 6051c86d0..07fabbe2d 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -1,11 +1,17 @@ +# encoding: utf-8 + module Heroku module Helpers extend self def home_directory - return Dir.home if defined? Dir.home # Ruby 1.9+ - running_on_windows? ? ENV['USERPROFILE'].gsub("\\","/") : ENV['HOME'] + if running_on_windows? + # https://bugs.ruby-lang.org/issues/10126 + Dir.home.force_encoding('cp775') + else + Dir.home + end end def running_on_windows? From 3d232bf34365db1c06bbd442d332eedd41f75634 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 10 Apr 2015 15:30:12 -0700 Subject: [PATCH 435/952] do not exit if the only issue is missing creds in netrc --- lib/heroku/command/base.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 534415fda..c11f83930 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -251,7 +251,6 @@ def git_url(app_name) else unless has_http_git_entry_in_netrc warn "WARNING: Incomplete credentials detected, git may not work with Heroku. Run `heroku login` to update your credentials. See documentation for details: https://devcenter.heroku.com/articles/http-git#authentication" - exit 1 end "https://#{Heroku::Auth.http_git_host}/#{app_name}.git" end From 03061d8e65daf5e13ef9b6da34c2da641c178d36 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Thu, 9 Apr 2015 17:05:45 -0500 Subject: [PATCH 436/952] Replaced existing buildpack commands with basics of new multi-buildpack support, but keeping old external behavior --- lib/heroku/command/buildpack.rb | 41 ++++++++++++++++++++++----- spec/heroku/command/buildpack_spec.rb | 20 ++++++------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 4f23402f5..b48590119 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -33,32 +33,59 @@ def index # # set new app buildpack # + # -i, --index NUM # the 1-based index of the URL in the list of URLs + # + #Example: + # + # $ heroku buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby + # def set unless buildpack_url = shift_argument error("Usage: heroku buildpack:set BUILDPACK_URL.\nMust specify target buildpack URL.") end - api.put_app_buildpacks_v3(app, {:updates => [{:buildpack => buildpack_url}]}) + validate_arguments! + index = options[:index] || 1 + index -= 1 + + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + buildpack_urls = [] + + # overwrite the buildpack at index + if app_buildpacks.size >= index + app_buildpacks.each do |buildpack| + ordinal = buildpack["ordinal"] + if ordinal == index + buildpack_urls << buildpack_url + end + buildpack_urls << buildpack["buildpack"]["url"] + end + else + buildpack_urls << buildpack_url + end + + api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) display "Buildpack set. Next release on #{app} will use #{buildpack_url}." display "Run `git push heroku master` to create a new release using #{buildpack_url}." end - # buildpack:unset + # buildpack:clear # - # unset the app buildpack + # clear all buildpacks set on the app # - def unset + def clear api.put_app_buildpacks_v3(app, {:updates => []}) vars = api.get_config_vars(app).body if vars.has_key?("BUILDPACK_URL") - display "Buildpack unset." + display "Buildpack(s) cleared." warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" elsif vars.has_key?("LANGUAGE_PACK_URL") - display "Buildpack unset." + display "Buildpack(s) cleared." warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" else - display "Buildpack unset. Next release on #{app} will detect buildpack normally." + display "Buildpack(s) cleared. Next release on #{app} will detect buildpack normally." end end diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index f2fddfd3e..9823193af 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -73,34 +73,34 @@ module Heroku::Command end end - describe "unset" do - it "unsets the buildpack URL" do - stderr, stdout = execute("buildpack:unset") + describe "clear" do + it "clears the buildpack URL" do + stderr, stdout = execute("buildpack:clear") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Buildpack unset. Next release on example will detect buildpack normally. +Buildpack(s) cleared. Next release on example will detect buildpack normally. STDOUT end - it "unsets and warns about buildpack URL config var" do + it "clears and warns about buildpack URL config var" do execute("config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:unset") + stderr, stdout = execute("buildpack:clear") expect(stderr).to eq <<-STDERR WARNING: The BUILDPACK_URL config var is still set and will be used for the next release STDERR expect(stdout).to eq <<-STDOUT -Buildpack unset. +Buildpack(s) cleared. STDOUT end - it "unsets and warns about language pack URL config var" do + it "clears and warns about language pack URL config var" do execute("config:set LANGUAGE_PACK_URL=https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:unset") + stderr, stdout = execute("buildpack:clear") expect(stderr).to eq <<-STDERR WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release STDERR expect(stdout).to eq <<-STDOUT -Buildpack unset. +Buildpack(s) cleared. STDOUT end end From 48a6759da7b5f4a18672406fc10e535d54b86098 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Fri, 10 Apr 2015 15:48:28 -0500 Subject: [PATCH 437/952] Added many tests for buildpack:set command. Improved implementation to accomidate tests --- lib/heroku/command/buildpack.rb | 22 +++--- spec/heroku/command/buildpack_spec.rb | 101 ++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 12 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index b48590119..b43b56d05 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -45,23 +45,21 @@ def set end validate_arguments! - index = options[:index] || 1 + index = (options[:index] || 1).to_i index -= 1 app_buildpacks = api.get_app_buildpacks_v3(app)[:body] - buildpack_urls = [] - - # overwrite the buildpack at index - if app_buildpacks.size >= index - app_buildpacks.each do |buildpack| - ordinal = buildpack["ordinal"] - if ordinal == index - buildpack_urls << buildpack_url - end - buildpack_urls << buildpack["buildpack"]["url"] + buildpack_urls = app_buildpacks.map do |buildpack| + ordinal = buildpack["ordinal"].to_i + if ordinal == index + buildpack_url + else + buildpack["buildpack"]["url"] end - else + end + + if app_buildpacks.size <= index buildpack_urls << buildpack_url end diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 9823193af..f2ca3ca17 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -4,6 +4,15 @@ module Heroku::Command describe Buildpack do + def stub_put(*buildpacks) + Excon.stub({ + :method => :put, + :path => "/apps/example/buildpack-installations", + :body => {"updates" => buildpacks.map{|bp| {"buildpack" => bp}}}.to_json + }, + {:status => 200}) + end + before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar-14") @@ -71,6 +80,98 @@ module Heroku::Command STDERR expect(stdout).to eq("") end + + it "sets the buildpack URL with index" do + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. + STDOUT + end + + context "with one existing buildpack" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, + { + :body => [ + { + "buildpack" => { + "url" => "https://github.com/heroku/heroku-buildpack-java" + }, + "ordinal" => 0 + } + ], + :status => 200 + }) + end + + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby" + ) + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. + STDOUT + end + end + + context "with two existing buildpack" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, + { + :body => [ + { + "buildpack" => { + "url" => "https://github.com/heroku/heroku-buildpack-java" + }, + "ordinal" => 0 + }, + { + "buildpack" => { + "url" => "https://github.com/heroku/heroku-buildpack-nodejs" + }, + "ordinal" => 1 + } + ], + :status => 200 + }) + end + + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs" + ) + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. + STDOUT + end + + it "adds buildpack URL to the end of list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby" + ) + stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. + STDOUT + end + end end describe "clear" do From b6c3ac3a06efc63255c1e108f5e83f542511ce9f Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Fri, 10 Apr 2015 16:28:02 -0500 Subject: [PATCH 438/952] Improved buildpack listing to accomidate multiple buildpacks, and to be consistent between commands --- lib/heroku/command/buildpack.rb | 27 +++++++++++++++++++++++---- spec/heroku/command/buildpack_spec.rb | 11 ++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index b43b56d05..db0f5ac51 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -24,8 +24,8 @@ def index if app_buildpacks.nil? or app_buildpacks.empty? display("#{app} has no Buildpack URL set.") else - styled_header("#{app} Buildpack URL") - display(app_buildpacks.first["buildpack"]["url"]) + styled_header("#{app} Buildpack URL#{app_buildpacks.size > 1 ? 's' : ''}") + display_buildpacks(app_buildpacks.map{|bp| bp["buildpack"]["url"]}) end end @@ -59,12 +59,19 @@ def set end end - if app_buildpacks.size <= index + # default behavior if index is out of range, or list is previously empty + # is to add buildpack to the list + if index < 0 or app_buildpacks.size <= index buildpack_urls << buildpack_url end api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) - display "Buildpack set. Next release on #{app} will use #{buildpack_url}." + if (buildpack_urls.size > 1) + display "Buildpack set. Next release on #{app} will use:" + display_buildpacks(buildpack_urls) + else + display "Buildpack set. Next release on #{app} will use #{buildpack_url}." + end display "Run `git push heroku master` to create a new release using #{buildpack_url}." end @@ -87,5 +94,17 @@ def clear end end + private + + def display_buildpacks(buildpacks) + if (buildpacks.size == 1) + display(" #{buildpacks.first}") + else + buildpacks.each_with_index do |bp, i| + display(" #{i+1}. #{bp}") + end + end + end + end end diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index f2ca3ca17..c15b93021 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -38,7 +38,7 @@ def stub_put(*buildpacks) expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === example Buildpack URL -https://github.com/heroku/heroku-buildpack-ruby + https://github.com/heroku/heroku-buildpack-ruby STDOUT end @@ -153,7 +153,9 @@ def stub_put(*buildpacks) stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Buildpack set. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-ruby + 2. https://github.com/heroku/heroku-buildpack-nodejs Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. STDOUT end @@ -167,7 +169,10 @@ def stub_put(*buildpacks) stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Buildpack set. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs + 3. https://github.com/heroku/heroku-buildpack-ruby Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. STDOUT end From 7e7401e833f46a68772a6a4c2507e23c8920120f Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 08:28:31 -0500 Subject: [PATCH 439/952] Added an buildpack:add command and tests. reworked text output for all commands to be more accurate --- lib/heroku/command/buildpack.rb | 54 +++++++- spec/heroku/command/buildpack_spec.rb | 177 +++++++++++++++----------- 2 files changed, 157 insertions(+), 74 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index db0f5ac51..e4c480edb 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -31,7 +31,7 @@ def index # buildpack:set BUILDPACK_URL # - # set new app buildpack + # set new app buildpack, overwriting into list of buildpacks if neccessary # # -i, --index NUM # the 1-based index of the URL in the list of URLs # @@ -69,10 +69,58 @@ def set if (buildpack_urls.size > 1) display "Buildpack set. Next release on #{app} will use:" display_buildpacks(buildpack_urls) + display "Run `git push heroku master` to create a new release using these buildpacks." else display "Buildpack set. Next release on #{app} will use #{buildpack_url}." + display "Run `git push heroku master` to create a new release using this buildpack." + end + end + + # buildpack:add BUILDPACK_URL + # + # add new app buildpack, inserting into list of buildpacks if neccessary + # + # -i, --index NUM # the 1-based index of the URL in the list of URLs + # + #Example: + # + # $ heroku buildpack:add -i 1 https://github.com/heroku/heroku-buildpack-ruby + # + def add + unless buildpack_url = shift_argument + error("Usage: heroku buildpack:add BUILDPACK_URL.\nMust specify target buildpack URL.") + end + + validate_arguments! + index = (options[:index] || 1).to_i + index -= 1 + + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + buildpack_urls = app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"].to_i + if ordinal == index + [buildpack_url, buildpack["buildpack"]["url"]] + else + buildpack["buildpack"]["url"] + end + }.flatten + + # default behavior if index is out of range, or list is previously empty + # is to add buildpack to the list + if index < 0 or app_buildpacks.size <= index + buildpack_urls << buildpack_url + end + + api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) + if (buildpack_urls.size > 1) + display "Buildpack added. Next release on #{app} will use:" + display_buildpacks(buildpack_urls) + display "Run `git push heroku master` to create a new release using these buildpacks." + else + display "Buildpack added. Next release on #{app} will use #{buildpack_url}." + display "Run `git push heroku master` to create a new release using this buildpack." end - display "Run `git push heroku master` to create a new release using #{buildpack_url}." end # buildpack:clear @@ -98,7 +146,7 @@ def clear def display_buildpacks(buildpacks) if (buildpacks.size == 1) - display(" #{buildpacks.first}") + display(buildpacks.first) else buildpacks.each_with_index do |bp, i| display(" #{i+1}. #{bp}") diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index c15b93021..41d422a36 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -13,17 +13,28 @@ def stub_put(*buildpacks) {:status => 200}) end + def stub_get(*buildpacks) + Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, + { + :body => buildpacks.map.with_index { |bp, i| + { + "buildpack" => { + "url" => bp + }, + "ordinal" => i + } + }, + :status => 200 + }) + end + before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar-14") Excon.stub({:method => :put, :path => "/apps/example/buildpack-installations"}, {:status => 200}) - Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, - { - :body => [{"buildpack" => { "url" => "https://github.com/heroku/heroku-buildpack-ruby"}}], - :status => 200 - }) + stub_get("https://github.com/heroku/heroku-buildpack-ruby") end after(:each) do @@ -38,18 +49,14 @@ def stub_put(*buildpacks) expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === example Buildpack URL - https://github.com/heroku/heroku-buildpack-ruby +https://github.com/heroku/heroku-buildpack-ruby STDOUT end context "with no buildpack URL set" do before(:each) do Excon.stubs.shift - Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, - { - :body => [], - :status => 200 - }) + stub_get end it "does not display a buildpack URL" do @@ -68,7 +75,7 @@ def stub_put(*buildpacks) expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. -Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. STDOUT end @@ -86,7 +93,7 @@ def stub_put(*buildpacks) expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. -Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. STDOUT end @@ -94,18 +101,7 @@ def stub_put(*buildpacks) before(:each) do Excon.stubs.shift Excon.stubs.shift - Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, - { - :body => [ - { - "buildpack" => { - "url" => "https://github.com/heroku/heroku-buildpack-java" - }, - "ordinal" => 0 - } - ], - :status => 200 - }) + stub_get("https://github.com/heroku/heroku-buildpack-java") end it "overwrites an existing buildpack URL at index" do @@ -116,67 +112,106 @@ def stub_put(*buildpacks) expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. -Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. STDOUT end end - context "with two existing buildpack" do + context "with two existing buildpacks" do before(:each) do Excon.stubs.shift Excon.stubs.shift - Excon.stub({:method => :get, :path => "/apps/example/buildpack-installations"}, - { - :body => [ - { - "buildpack" => { - "url" => "https://github.com/heroku/heroku-buildpack-java" - }, - "ordinal" => 0 - }, - { - "buildpack" => { - "url" => "https://github.com/heroku/heroku-buildpack-nodejs" - }, - "ordinal" => 1 - } - ], - :status => 200 - }) - end - - it "overwrites an existing buildpack URL at index" do - stub_put( - "https://github.com/heroku/heroku-buildpack-ruby", - "https://github.com/heroku/heroku-buildpack-nodejs" - ) - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs" + ) + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-ruby 2. https://github.com/heroku/heroku-buildpack-nodejs -Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. - STDOUT - end - - it "adds buildpack URL to the end of list" do - stub_put( - "https://github.com/heroku/heroku-buildpack-java", - "https://github.com/heroku/heroku-buildpack-nodejs", - "https://github.com/heroku/heroku-buildpack-ruby" - ) - stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds buildpack URL to the end of list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby" + ) + stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-java 2. https://github.com/heroku/heroku-buildpack-nodejs 3. https://github.com/heroku/heroku-buildpack-ruby -Run `git push heroku master` to create a new release using https://github.com/heroku/heroku-buildpack-ruby. - STDOUT - end +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + end + + describe "add" do + context "with no buildpacks" do + before(:each) do + Excon.stubs.shift + stub_get + end + + it "adds the buildpack URL" do + stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "handles a missing buildpack URL arg" do + stderr, stdout = execute("buildpack:add") + expect(stderr).to eq <<-STDERR + ! Usage: heroku buildpack:add BUILDPACK_URL. + ! Must specify target buildpack URL. + STDERR + expect(stdout).to eq("") + end + + it "adds the buildpack URL with index" do + stderr, stdout = execute("buildpack:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT end + end + + context "with one existing buildpack" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end + + it "inserts a buildpack URL at index" do + stub_put("https://github.com/heroku/heroku-buildpack-ruby", "https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpack:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-ruby + 2. https://github.com/heroku/heroku-buildpack-java +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end end describe "clear" do From 958a70bd894e0ab36526721a94e0db64c9e4d993 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 08:52:54 -0500 Subject: [PATCH 440/952] Added more tests for buildpack:add command --- lib/heroku/command/buildpack.rb | 2 +- spec/heroku/command/buildpack_spec.rb | 53 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index e4c480edb..b41e3ed48 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -92,7 +92,7 @@ def add end validate_arguments! - index = (options[:index] || 1).to_i + index = (options[:index] || -1).to_i index -= 1 app_buildpacks = api.get_app_buildpacks_v3(app)[:body] diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 41d422a36..844dbe0ed 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -208,6 +208,59 @@ def stub_get(*buildpacks) Buildpack added. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-ruby 2. https://github.com/heroku/heroku-buildpack-java +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds a buildpack URL to the end of the list" do + stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + + context "with two existing buildpacks" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "inserts a buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpack:add -i 2 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-ruby + 3. https://github.com/heroku/heroku-buildpack-nodejs +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds a buildpack URL to the end of the list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs + 3. https://github.com/heroku/heroku-buildpack-ruby Run `git push heroku master` to create a new release using these buildpacks. STDOUT end From 004ae5a7668b426fe1636db601eb5e7c16f31292 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 09:43:19 -0500 Subject: [PATCH 441/952] Added a buildpack:remove command and several negative tests --- lib/heroku/command/buildpack.rb | 72 ++++++++++++++++++++++++++- spec/heroku/command/buildpack_spec.rb | 67 +++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index b41e3ed48..94eea5383 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -66,7 +66,7 @@ def set end api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) - if (buildpack_urls.size > 1) + if buildpack_urls.size > 1 display "Buildpack set. Next release on #{app} will use:" display_buildpacks(buildpack_urls) display "Run `git push heroku master` to create a new release using these buildpacks." @@ -113,7 +113,7 @@ def add end api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) - if (buildpack_urls.size > 1) + if buildpack_urls.size > 1 display "Buildpack added. Next release on #{app} will use:" display_buildpacks(buildpack_urls) display "Run `git push heroku master` to create a new release using these buildpacks." @@ -123,6 +123,74 @@ def add end end + # buildpack:remove [BUILDPACK_URL] + # + # remove a buildpack set on the app + # + # -i, --index NUM # the 1-based index of the URL to remove from the list of URLs + # + def remove + if buildpack_url = shift_argument + if options[:index] + error("Please choose either index or Buildpack URL, but not both, as arguments to this command!") + end + else + validate_arguments! + index = options[:index].to_i - 1 + end + + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + if app_buildpacks.size == 0 + error("No buildpacks were found. Next release on #{app} will detect buildpack normally.") + end + + if index and (index < 0 or index > app_buildpacks.size) + if app_buildpacks.size == 1 + error("Invalid index. Only valid value is 1.") + else + error("Invalid index. Please choose a value between 1 and #{app_buildpacks.size}") + end + end + + buildpack_urls = app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"].to_i + if ordinal == index + nil + elsif buildpack["buildpack"]["url"] == buildpack_url + nil + else + buildpack["buildpack"]["url"] + end + }.compact + + if buildpack_urls.size == app_buildpacks.size + error("Buildpack not found. Nothing was removed.") + end + + api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) + + if buildpack_urls.size > 1 + display "Buildpack removed. Next release on #{app} will use:" + display_buildpacks(buildpack_urls) + display "Run `git push heroku master` to create a new release using these buildpacks." + elsif buildpack_urls.size == 1 + display "Buildpack removed. Next release on #{app} will use #{buildpack_url}." + display "Run `git push heroku master` to create a new release using this buildpack." + else + vars = api.get_config_vars(app).body + if vars.has_key?("BUILDPACK_URL") + display "Buildpack removed." + warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" + elsif vars.has_key?("LANGUAGE_PACK_URL") + display "Buildpack removed." + warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" + else + display "Buildpack removed. Next release on #{app} will detect buildpack normally." + end + end + end + # buildpack:clear # # clear all buildpacks set on the app diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 844dbe0ed..7d4b6cdc9 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -298,5 +298,72 @@ def stub_get(*buildpacks) STDOUT end end + + describe "remove" do + context "with no buildpacks" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get + end + + it "reports an error removing index" do + stderr, stdout = execute("buildpack:remove -i 1") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! No buildpacks were found. Next release on example will detect buildpack normally. + STDOUT + end + + it "reports an error removing buildpack_url" do + stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! No buildpacks were found. Next release on example will detect buildpack normally. + STDOUT + end + end + + context "with one buildpack" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end + + it "reports an error index is out of range" do + stderr, stdout = execute("buildpack:remove -i 9") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Invalid index. Only valid value is 1. + STDOUT + end + + it "reports an error buildpack_url is not found" do + stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-foobar") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Buildpack not found. Nothing was removed. + STDOUT + end + end + + context "with two buildpack" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "reports an error index is out of range" do + stderr, stdout = execute("buildpack:remove -i 9") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Invalid index. Please choose a value between 1 and 2 + STDOUT + end + + end + end end end From b3e3ec68d1f97ad3032dfe239f393ea67efe5b09 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 09:47:25 -0500 Subject: [PATCH 442/952] Fixed a bug in buildpack tests that was removed stubs --- lib/heroku/command/buildpack.rb | 2 +- spec/heroku/command/buildpack_spec.rb | 67 +++++++++++++++------------ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 94eea5383..d9db98618 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -132,7 +132,7 @@ def add def remove if buildpack_url = shift_argument if options[:index] - error("Please choose either index or Buildpack URL, but not both, as arguments to this command!") + error("Please choose either index or Buildpack URL, but not both, as arguments to this command.") end else validate_arguments! diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 7d4b6cdc9..28723a4e6 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -302,7 +302,6 @@ def stub_get(*buildpacks) describe "remove" do context "with no buildpacks" do before(:each) do - Excon.stubs.shift Excon.stubs.shift stub_get end @@ -325,42 +324,52 @@ def stub_get(*buildpacks) end context "with one buildpack" do - before(:each) do - Excon.stubs.shift - Excon.stubs.shift - stub_get("https://github.com/heroku/heroku-buildpack-java") - end - - it "reports an error index is out of range" do - stderr, stdout = execute("buildpack:remove -i 9") - expect(stdout).to eq("") - expect(stderr).to eq <<-STDOUT + context "reports and error" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end + + it "invalid arguments" do + stderr, stdout = execute("buildpack:remove -i 1 https://github.com/heroku/heroku-buildpack-java") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Please choose either index or Buildpack URL, but not both, as arguments to this command. + STDOUT + end + + it "index is out of range" do + stderr, stdout = execute("buildpack:remove -i 9") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT ! Invalid index. Only valid value is 1. - STDOUT - end + STDOUT + end - it "reports an error buildpack_url is not found" do - stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-foobar") - expect(stdout).to eq("") - expect(stderr).to eq <<-STDOUT + it "buildpack_url is not found" do + stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-foobar") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT ! Buildpack not found. Nothing was removed. - STDOUT + STDOUT + end end end context "with two buildpack" do - before(:each) do - Excon.stubs.shift - Excon.stubs.shift - stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") - end - - it "reports an error index is out of range" do - stderr, stdout = execute("buildpack:remove -i 9") - expect(stdout).to eq("") - expect(stderr).to eq <<-STDOUT + context "reports and error" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "index is out of range" do + stderr, stdout = execute("buildpack:remove -i 9") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT ! Invalid index. Please choose a value between 1 and 2 - STDOUT + STDOUT + end end end From 16ff9e794b79405f47c7348849a29e2e2c8bae14 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 10:02:29 -0500 Subject: [PATCH 443/952] Added many more positive tests for buildpack:remove --- lib/heroku/command/buildpack.rb | 2 +- spec/heroku/command/buildpack_spec.rb | 117 ++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index d9db98618..562fd76ad 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -175,7 +175,7 @@ def remove display_buildpacks(buildpack_urls) display "Run `git push heroku master` to create a new release using these buildpacks." elsif buildpack_urls.size == 1 - display "Buildpack removed. Next release on #{app} will use #{buildpack_url}." + display "Buildpack removed. Next release on #{app} will use #{buildpack_urls.first}." display "Run `git push heroku master` to create a new release using this buildpack." else vars = api.get_config_vars(app).body diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 28723a4e6..5d123adb3 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -324,13 +324,38 @@ def stub_get(*buildpacks) end context "with one buildpack" do - context "reports and error" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-ruby") + stub_put + end + + it "removes index" do + stderr, stdout = execute("buildpack:remove -i 1") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will detect buildpack normally. + STDOUT + end + + it "removes url" do + stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will detect buildpack normally. + STDOUT + end + end + + context "unsuccessfully" do before(:each) do Excon.stubs.shift stub_get("https://github.com/heroku/heroku-buildpack-java") end - it "invalid arguments" do + it "validates arguments" do stderr, stdout = execute("buildpack:remove -i 1 https://github.com/heroku/heroku-buildpack-java") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT @@ -338,7 +363,7 @@ def stub_get(*buildpacks) STDOUT end - it "index is out of range" do + it "checks if index is in range" do stderr, stdout = execute("buildpack:remove -i 9") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT @@ -346,7 +371,7 @@ def stub_get(*buildpacks) STDOUT end - it "buildpack_url is not found" do + it "checks if buildpack_url is found" do stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-foobar") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT @@ -356,14 +381,52 @@ def stub_get(*buildpacks) end end - context "with two buildpack" do - context "reports and error" do + context "with two buildpacks" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") + end + + it "removes index" do + stub_put("https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpack:remove -i 2") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-java. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "removes index" do + stub_put("https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:remove -i 1") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + + it "removes url" do + stub_put("https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-java. +Run `git push heroku master` to create a new release using this buildpack. + STDOUT + end + end + + context "unsuccessfully" do before(:each) do Excon.stubs.shift stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") end - it "index is out of range" do + it "checks if index is in range" do stderr, stdout = execute("buildpack:remove -i 9") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT @@ -371,7 +434,47 @@ def stub_get(*buildpacks) STDOUT end end + end + + context "with three buildpacks" do + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + end + it "removes index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:remove -i 2") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "removes url" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack removed. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end end end end From 7d10f99e6b30c03824f3a88840fd47b1b037ff41 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 10:09:23 -0500 Subject: [PATCH 444/952] Refactored buildpack update code to reuse redundancy --- lib/heroku/command/buildpack.rb | 84 ++++++++++++++------------------- 1 file changed, 35 insertions(+), 49 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 562fd76ad..504b91123 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -65,15 +65,7 @@ def set buildpack_urls << buildpack_url end - api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) - if buildpack_urls.size > 1 - display "Buildpack set. Next release on #{app} will use:" - display_buildpacks(buildpack_urls) - display "Run `git push heroku master` to create a new release using these buildpacks." - else - display "Buildpack set. Next release on #{app} will use #{buildpack_url}." - display "Run `git push heroku master` to create a new release using this buildpack." - end + update_buildpacks(buildpack_urls, "set") end # buildpack:add BUILDPACK_URL @@ -112,15 +104,7 @@ def add buildpack_urls << buildpack_url end - api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) - if buildpack_urls.size > 1 - display "Buildpack added. Next release on #{app} will use:" - display_buildpacks(buildpack_urls) - display "Run `git push heroku master` to create a new release using these buildpacks." - else - display "Buildpack added. Next release on #{app} will use #{buildpack_url}." - display "Run `git push heroku master` to create a new release using this buildpack." - end + update_buildpacks(buildpack_urls, "added") end # buildpack:remove [BUILDPACK_URL] @@ -168,27 +152,8 @@ def remove error("Buildpack not found. Nothing was removed.") end - api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) + update_buildpacks(buildpack_urls, "removed") - if buildpack_urls.size > 1 - display "Buildpack removed. Next release on #{app} will use:" - display_buildpacks(buildpack_urls) - display "Run `git push heroku master` to create a new release using these buildpacks." - elsif buildpack_urls.size == 1 - display "Buildpack removed. Next release on #{app} will use #{buildpack_urls.first}." - display "Run `git push heroku master` to create a new release using this buildpack." - else - vars = api.get_config_vars(app).body - if vars.has_key?("BUILDPACK_URL") - display "Buildpack removed." - warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" - elsif vars.has_key?("LANGUAGE_PACK_URL") - display "Buildpack removed." - warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" - else - display "Buildpack removed. Next release on #{app} will detect buildpack normally." - end - end end # buildpack:clear @@ -197,21 +162,16 @@ def remove # def clear api.put_app_buildpacks_v3(app, {:updates => []}) - - vars = api.get_config_vars(app).body - if vars.has_key?("BUILDPACK_URL") - display "Buildpack(s) cleared." - warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" - elsif vars.has_key?("LANGUAGE_PACK_URL") - display "Buildpack(s) cleared." - warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" - else - display "Buildpack(s) cleared. Next release on #{app} will detect buildpack normally." - end + display_no_buildpacks("(s) cleared") end private + def update_buildpacks(buildpack_urls, action) + api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) + display_buildpack_change(buildpack_urls, action) + end + def display_buildpacks(buildpacks) if (buildpacks.size == 1) display(buildpacks.first) @@ -222,5 +182,31 @@ def display_buildpacks(buildpacks) end end + def display_buildpack_change(buildpack_urls, action) + if buildpack_urls.size > 1 + display "Buildpack #{action}. Next release on #{app} will use:" + display_buildpacks(buildpack_urls) + display "Run `git push heroku master` to create a new release using these buildpacks." + elsif buildpack_urls.size == 1 + display "Buildpack #{action}. Next release on #{app} will use #{buildpack_urls.first}." + display "Run `git push heroku master` to create a new release using this buildpack." + else + display_no_buildpacks + end + end + + def display_no_buildpacks(action=" removed") + vars = api.get_config_vars(app).body + if vars.has_key?("BUILDPACK_URL") + display "Buildpack#{action}." + warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" + elsif vars.has_key?("LANGUAGE_PACK_URL") + display "Buildpack#{action}." + warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" + else + display "Buildpack#{action}. Next release on #{app} will detect buildpack normally." + end + end + end end From ad4f16e346e650d7ee40af45f0926d9751c4e1d4 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 10:22:32 -0500 Subject: [PATCH 445/952] Cleaned up whitespace --- lib/heroku/command/buildpack.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 504b91123..4927c8357 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -153,7 +153,6 @@ def remove end update_buildpacks(buildpack_urls, "removed") - end # buildpack:clear From b047fa33f4163065c077fa64a9e5fe70a9cc275f Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 13:43:56 -0500 Subject: [PATCH 446/952] Added a check for duplicate buildpacks --- lib/heroku/command/buildpack.rb | 16 +- spec/heroku/command/buildpack_spec.rb | 215 ++++++++++++++++---------- 2 files changed, 146 insertions(+), 85 deletions(-) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpack.rb index 4927c8357..fa5064170 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpack.rb @@ -52,10 +52,13 @@ def set buildpack_urls = app_buildpacks.map do |buildpack| ordinal = buildpack["ordinal"].to_i - if ordinal == index + existing_url = buildpack["buildpack"]["url"] + if existing_url == buildpack_url + error("The buildpack #{buildpack_url} is already set on your app.") + elsif ordinal == index buildpack_url else - buildpack["buildpack"]["url"] + existing_url end end @@ -91,10 +94,13 @@ def add buildpack_urls = app_buildpacks.map { |buildpack| ordinal = buildpack["ordinal"].to_i - if ordinal == index - [buildpack_url, buildpack["buildpack"]["url"]] + existing_url = buildpack["buildpack"]["url"] + if existing_url == buildpack_url + error("The buildpack #{buildpack_url} is already set on your app.") + elsif ordinal == index + [buildpack_url, existing_url] else - buildpack["buildpack"]["url"] + existing_url end }.flatten diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpack_spec.rb index 5d123adb3..b2009aad6 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpack_spec.rb @@ -70,90 +70,128 @@ def stub_get(*buildpacks) end describe "set" do - it "sets the buildpack URL" do - stderr, stdout = execute("buildpack:set https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + context "with no buildpacks" do + before do + Excon.stubs.shift + stub_get + end + + it "sets the buildpack URL" do + stderr, stdout = execute("buildpack:set https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. Run `git push heroku master` to create a new release using this buildpack. - STDOUT - end + STDOUT + end - it "handles a missing buildpack URL arg" do - stderr, stdout = execute("buildpack:set") - expect(stderr).to eq <<-STDERR + it "handles a missing buildpack URL arg" do + stderr, stdout = execute("buildpack:set") + expect(stderr).to eq <<-STDERR ! Usage: heroku buildpack:set BUILDPACK_URL. ! Must specify target buildpack URL. - STDERR - expect(stdout).to eq("") - end + STDERR + expect(stdout).to eq("") + end - it "sets the buildpack URL with index" do - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "sets the buildpack URL with index" do + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. Run `git push heroku master` to create a new release using this buildpack. - STDOUT + STDOUT + end end context "with one existing buildpack" do - before(:each) do - Excon.stubs.shift - Excon.stubs.shift - stub_get("https://github.com/heroku/heroku-buildpack-java") - end + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java") + end - it "overwrites an existing buildpack URL at index" do - stub_put( - "https://github.com/heroku/heroku-buildpack-ruby" - ) - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. Run `git push heroku master` to create a new release using this buildpack. - STDOUT + STDOUT + end + end + + context "unsuccessfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-ruby") + end + + it "fails if buildpack is already set" do + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! The buildpack https://github.com/heroku/heroku-buildpack-ruby is already set on your app. + STDOUT + end end end context "with two existing buildpacks" do - before(:each) do - Excon.stubs.shift - Excon.stubs.shift - stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") - end + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end - it "overwrites an existing buildpack URL at index" do - stub_put( - "https://github.com/heroku/heroku-buildpack-ruby", - "https://github.com/heroku/heroku-buildpack-nodejs" - ) - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "overwrites an existing buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-ruby 2. https://github.com/heroku/heroku-buildpack-nodejs Run `git push heroku master` to create a new release using these buildpacks. - STDOUT - end + STDOUT + end - it "adds buildpack URL to the end of list" do - stub_put( - "https://github.com/heroku/heroku-buildpack-java", - "https://github.com/heroku/heroku-buildpack-nodejs", - "https://github.com/heroku/heroku-buildpack-ruby" - ) - stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "adds buildpack URL to the end of list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-java 2. https://github.com/heroku/heroku-buildpack-nodejs 3. https://github.com/heroku/heroku-buildpack-ruby Run `git push heroku master` to create a new release using these buildpacks. - STDOUT + STDOUT + end + end + + context "unsuccessfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "fails if buildpack is already set" do + stderr, stdout = execute("buildpack:set -i 2 https://github.com/heroku/heroku-buildpack-java") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! The buildpack https://github.com/heroku/heroku-buildpack-java is already set on your app. + STDOUT + end end end end @@ -226,43 +264,60 @@ def stub_get(*buildpacks) end context "with two existing buildpacks" do - before(:each) do - Excon.stubs.shift - Excon.stubs.shift - stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") - end + context "successfully" do + before(:each) do + Excon.stubs.shift + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end - it "inserts a buildpack URL at index" do - stub_put( - "https://github.com/heroku/heroku-buildpack-java", - "https://github.com/heroku/heroku-buildpack-ruby", - "https://github.com/heroku/heroku-buildpack-nodejs") - stderr, stdout = execute("buildpack:add -i 2 https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "inserts a buildpack URL at index" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-ruby", + "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpack:add -i 2 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-java 2. https://github.com/heroku/heroku-buildpack-ruby 3. https://github.com/heroku/heroku-buildpack-nodejs Run `git push heroku master` to create a new release using these buildpacks. - STDOUT - end + STDOUT + end - it "adds a buildpack URL to the end of the list" do - stub_put( - "https://github.com/heroku/heroku-buildpack-java", - "https://github.com/heroku/heroku-buildpack-nodejs", - "https://github.com/heroku/heroku-buildpack-ruby") - stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") - stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "adds a buildpack URL to the end of the list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use: 1. https://github.com/heroku/heroku-buildpack-java 2. https://github.com/heroku/heroku-buildpack-nodejs 3. https://github.com/heroku/heroku-buildpack-ruby Run `git push heroku master` to create a new release using these buildpacks. - STDOUT + STDOUT + end + end + + context "successfully" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") + end + + it "inserts a buildpack URL at index" do + stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-java") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! The buildpack https://github.com/heroku/heroku-buildpack-java is already set on your app. + STDOUT + end end end end From 87691faba9af20a9608b7864a53772f1f4761eae Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 13:49:20 -0500 Subject: [PATCH 447/952] Renamed buildpack command to buildpacks --- .../command/{buildpack.rb => buildpacks.rb} | 24 +++--- .../{buildpack_spec.rb => buildpacks_spec.rb} | 76 +++++++++---------- 2 files changed, 50 insertions(+), 50 deletions(-) rename lib/heroku/command/{buildpack.rb => buildpacks.rb} (90%) rename spec/heroku/command/{buildpack_spec.rb => buildpacks_spec.rb} (85%) diff --git a/lib/heroku/command/buildpack.rb b/lib/heroku/command/buildpacks.rb similarity index 90% rename from lib/heroku/command/buildpack.rb rename to lib/heroku/command/buildpacks.rb index fa5064170..3dee6143b 100644 --- a/lib/heroku/command/buildpack.rb +++ b/lib/heroku/command/buildpacks.rb @@ -5,15 +5,15 @@ module Heroku::Command # manage the buildpack for an app # - class Buildpack < Base + class Buildpacks < Base - # buildpack + # buildpacks # - # display the buildpack_url for an app + # display the buildpack_url(s) for an app # #Examples: # - # $ heroku buildpack + # $ heroku buildpacks # https://github.com/heroku/heroku-buildpack-ruby # def index @@ -29,7 +29,7 @@ def index end end - # buildpack:set BUILDPACK_URL + # buildpacks:set BUILDPACK_URL # # set new app buildpack, overwriting into list of buildpacks if neccessary # @@ -37,11 +37,11 @@ def index # #Example: # - # $ heroku buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby + # $ heroku buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby # def set unless buildpack_url = shift_argument - error("Usage: heroku buildpack:set BUILDPACK_URL.\nMust specify target buildpack URL.") + error("Usage: heroku buildpacks:set BUILDPACK_URL.\nMust specify target buildpack URL.") end validate_arguments! @@ -71,7 +71,7 @@ def set update_buildpacks(buildpack_urls, "set") end - # buildpack:add BUILDPACK_URL + # buildpacks:add BUILDPACK_URL # # add new app buildpack, inserting into list of buildpacks if neccessary # @@ -79,11 +79,11 @@ def set # #Example: # - # $ heroku buildpack:add -i 1 https://github.com/heroku/heroku-buildpack-ruby + # $ heroku buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-ruby # def add unless buildpack_url = shift_argument - error("Usage: heroku buildpack:add BUILDPACK_URL.\nMust specify target buildpack URL.") + error("Usage: heroku buildpacks:add BUILDPACK_URL.\nMust specify target buildpack URL.") end validate_arguments! @@ -113,7 +113,7 @@ def add update_buildpacks(buildpack_urls, "added") end - # buildpack:remove [BUILDPACK_URL] + # buildpacks:remove [BUILDPACK_URL] # # remove a buildpack set on the app # @@ -161,7 +161,7 @@ def remove update_buildpacks(buildpack_urls, "removed") end - # buildpack:clear + # buildpacks:clear # # clear all buildpacks set on the app # diff --git a/spec/heroku/command/buildpack_spec.rb b/spec/heroku/command/buildpacks_spec.rb similarity index 85% rename from spec/heroku/command/buildpack_spec.rb rename to spec/heroku/command/buildpacks_spec.rb index b2009aad6..202b99c0b 100644 --- a/spec/heroku/command/buildpack_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -1,8 +1,8 @@ require "spec_helper" -require "heroku/command/buildpack" +require "heroku/command/buildpacks" module Heroku::Command - describe Buildpack do + describe Buildpacks do def stub_put(*buildpacks) Excon.stub({ @@ -45,7 +45,7 @@ def stub_get(*buildpacks) describe "index" do it "displays the buildpack URL" do - stderr, stdout = execute("buildpack") + stderr, stdout = execute("buildpacks") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT === example Buildpack URL @@ -60,7 +60,7 @@ def stub_get(*buildpacks) end it "does not display a buildpack URL" do - stderr, stdout = execute("buildpack") + stderr, stdout = execute("buildpacks") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT example has no Buildpack URL set. @@ -77,7 +77,7 @@ def stub_get(*buildpacks) end it "sets the buildpack URL" do - stderr, stdout = execute("buildpack:set https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. @@ -86,16 +86,16 @@ def stub_get(*buildpacks) end it "handles a missing buildpack URL arg" do - stderr, stdout = execute("buildpack:set") + stderr, stdout = execute("buildpacks:set") expect(stderr).to eq <<-STDERR - ! Usage: heroku buildpack:set BUILDPACK_URL. + ! Usage: heroku buildpacks:set BUILDPACK_URL. ! Must specify target buildpack URL. STDERR expect(stdout).to eq("") end it "sets the buildpack URL with index" do - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. @@ -115,7 +115,7 @@ def stub_get(*buildpacks) it "overwrites an existing buildpack URL at index" do stub_put( "https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. @@ -131,7 +131,7 @@ def stub_get(*buildpacks) end it "fails if buildpack is already set" do - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! The buildpack https://github.com/heroku/heroku-buildpack-ruby is already set on your app. @@ -152,7 +152,7 @@ def stub_get(*buildpacks) stub_put( "https://github.com/heroku/heroku-buildpack-ruby", "https://github.com/heroku/heroku-buildpack-nodejs") - stderr, stdout = execute("buildpack:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use: @@ -167,7 +167,7 @@ def stub_get(*buildpacks) "https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs", "https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 99 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack set. Next release on example will use: @@ -186,7 +186,7 @@ def stub_get(*buildpacks) end it "fails if buildpack is already set" do - stderr, stdout = execute("buildpack:set -i 2 https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpacks:set -i 2 https://github.com/heroku/heroku-buildpack-java") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! The buildpack https://github.com/heroku/heroku-buildpack-java is already set on your app. @@ -204,7 +204,7 @@ def stub_get(*buildpacks) end it "adds the buildpack URL" do - stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. @@ -213,16 +213,16 @@ def stub_get(*buildpacks) end it "handles a missing buildpack URL arg" do - stderr, stdout = execute("buildpack:add") + stderr, stdout = execute("buildpacks:add") expect(stderr).to eq <<-STDERR - ! Usage: heroku buildpack:add BUILDPACK_URL. + ! Usage: heroku buildpacks:add BUILDPACK_URL. ! Must specify target buildpack URL. STDERR expect(stdout).to eq("") end it "adds the buildpack URL with index" do - stderr, stdout = execute("buildpack:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. @@ -240,7 +240,7 @@ def stub_get(*buildpacks) it "inserts a buildpack URL at index" do stub_put("https://github.com/heroku/heroku-buildpack-ruby", "https://github.com/heroku/heroku-buildpack-java") - stderr, stdout = execute("buildpack:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use: @@ -252,7 +252,7 @@ def stub_get(*buildpacks) it "adds a buildpack URL to the end of the list" do stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use: @@ -276,7 +276,7 @@ def stub_get(*buildpacks) "https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby", "https://github.com/heroku/heroku-buildpack-nodejs") - stderr, stdout = execute("buildpack:add -i 2 https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add -i 2 https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use: @@ -293,7 +293,7 @@ def stub_get(*buildpacks) "https://github.com/heroku/heroku-buildpack-nodejs", "https://github.com/heroku/heroku-buildpack-ruby") stub_put("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") - stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack added. Next release on example will use: @@ -312,7 +312,7 @@ def stub_get(*buildpacks) end it "inserts a buildpack URL at index" do - stderr, stdout = execute("buildpack:add https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpacks:add https://github.com/heroku/heroku-buildpack-java") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! The buildpack https://github.com/heroku/heroku-buildpack-java is already set on your app. @@ -324,7 +324,7 @@ def stub_get(*buildpacks) describe "clear" do it "clears the buildpack URL" do - stderr, stdout = execute("buildpack:clear") + stderr, stdout = execute("buildpacks:clear") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack(s) cleared. Next release on example will detect buildpack normally. @@ -333,7 +333,7 @@ def stub_get(*buildpacks) it "clears and warns about buildpack URL config var" do execute("config:set BUILDPACK_URL=https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:clear") + stderr, stdout = execute("buildpacks:clear") expect(stderr).to eq <<-STDERR WARNING: The BUILDPACK_URL config var is still set and will be used for the next release STDERR @@ -344,7 +344,7 @@ def stub_get(*buildpacks) it "clears and warns about language pack URL config var" do execute("config:set LANGUAGE_PACK_URL=https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:clear") + stderr, stdout = execute("buildpacks:clear") expect(stderr).to eq <<-STDERR WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release STDERR @@ -362,7 +362,7 @@ def stub_get(*buildpacks) end it "reports an error removing index" do - stderr, stdout = execute("buildpack:remove -i 1") + stderr, stdout = execute("buildpacks:remove -i 1") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! No buildpacks were found. Next release on example will detect buildpack normally. @@ -370,7 +370,7 @@ def stub_get(*buildpacks) end it "reports an error removing buildpack_url" do - stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! No buildpacks were found. Next release on example will detect buildpack normally. @@ -388,7 +388,7 @@ def stub_get(*buildpacks) end it "removes index" do - stderr, stdout = execute("buildpack:remove -i 1") + stderr, stdout = execute("buildpacks:remove -i 1") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will detect buildpack normally. @@ -396,7 +396,7 @@ def stub_get(*buildpacks) end it "removes url" do - stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will detect buildpack normally. @@ -411,7 +411,7 @@ def stub_get(*buildpacks) end it "validates arguments" do - stderr, stdout = execute("buildpack:remove -i 1 https://github.com/heroku/heroku-buildpack-java") + stderr, stdout = execute("buildpacks:remove -i 1 https://github.com/heroku/heroku-buildpack-java") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! Please choose either index or Buildpack URL, but not both, as arguments to this command. @@ -419,7 +419,7 @@ def stub_get(*buildpacks) end it "checks if index is in range" do - stderr, stdout = execute("buildpack:remove -i 9") + stderr, stdout = execute("buildpacks:remove -i 9") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! Invalid index. Only valid value is 1. @@ -427,7 +427,7 @@ def stub_get(*buildpacks) end it "checks if buildpack_url is found" do - stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-foobar") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-foobar") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! Buildpack not found. Nothing was removed. @@ -446,7 +446,7 @@ def stub_get(*buildpacks) it "removes index" do stub_put("https://github.com/heroku/heroku-buildpack-java") - stderr, stdout = execute("buildpack:remove -i 2") + stderr, stdout = execute("buildpacks:remove -i 2") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-java. @@ -456,7 +456,7 @@ def stub_get(*buildpacks) it "removes index" do stub_put("https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:remove -i 1") + stderr, stdout = execute("buildpacks:remove -i 1") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-ruby. @@ -466,7 +466,7 @@ def stub_get(*buildpacks) it "removes url" do stub_put("https://github.com/heroku/heroku-buildpack-java") - stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will use https://github.com/heroku/heroku-buildpack-java. @@ -482,7 +482,7 @@ def stub_get(*buildpacks) end it "checks if index is in range" do - stderr, stdout = execute("buildpack:remove -i 9") + stderr, stdout = execute("buildpacks:remove -i 9") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT ! Invalid index. Please choose a value between 1 and 2 @@ -506,7 +506,7 @@ def stub_get(*buildpacks) stub_put( "https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") - stderr, stdout = execute("buildpack:remove -i 2") + stderr, stdout = execute("buildpacks:remove -i 2") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will use: @@ -520,7 +520,7 @@ def stub_get(*buildpacks) stub_put( "https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs") - stderr, stdout = execute("buildpack:remove https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:remove https://github.com/heroku/heroku-buildpack-ruby") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Buildpack removed. Next release on example will use: From e1d6198edfd8b82d45d15081e7e5ffd93440d842 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 16:22:41 -0500 Subject: [PATCH 448/952] Removed the parens around the pluralization of Buildpacks because it looked dumb --- lib/heroku/command/buildpacks.rb | 2 +- spec/heroku/command/buildpacks_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 3dee6143b..65057d71a 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -167,7 +167,7 @@ def remove # def clear api.put_app_buildpacks_v3(app, {:updates => []}) - display_no_buildpacks("(s) cleared") + display_no_buildpacks("s cleared") end private diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index 202b99c0b..154e46c2d 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -327,7 +327,7 @@ def stub_get(*buildpacks) stderr, stdout = execute("buildpacks:clear") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Buildpack(s) cleared. Next release on example will detect buildpack normally. +Buildpacks cleared. Next release on example will detect buildpack normally. STDOUT end @@ -338,7 +338,7 @@ def stub_get(*buildpacks) WARNING: The BUILDPACK_URL config var is still set and will be used for the next release STDERR expect(stdout).to eq <<-STDOUT -Buildpack(s) cleared. +Buildpacks cleared. STDOUT end @@ -349,7 +349,7 @@ def stub_get(*buildpacks) WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release STDERR expect(stdout).to eq <<-STDOUT -Buildpack(s) cleared. +Buildpacks cleared. STDOUT end end From a50ce40590989c20be42a322687f2491d83469f5 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 17:01:03 -0500 Subject: [PATCH 449/952] Refactored buildpacks command to consolidate common code for mutating --- lib/heroku/command/buildpacks.rb | 100 ++++++++++++------------- spec/heroku/command/buildpacks_spec.rb | 2 +- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 65057d71a..1a2ca2801 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -44,31 +44,21 @@ def set error("Usage: heroku buildpacks:set BUILDPACK_URL.\nMust specify target buildpack URL.") end - validate_arguments! - index = (options[:index] || 1).to_i - index -= 1 - - app_buildpacks = api.get_app_buildpacks_v3(app)[:body] - - buildpack_urls = app_buildpacks.map do |buildpack| - ordinal = buildpack["ordinal"].to_i - existing_url = buildpack["buildpack"]["url"] - if existing_url == buildpack_url - error("The buildpack #{buildpack_url} is already set on your app.") - elsif ordinal == index - buildpack_url - else - existing_url + index = get_index(1) + + mutate_buildpacks(buildpack_url, index, "set") do |app_buildpacks| + app_buildpacks.map do |buildpack| + ordinal = buildpack["ordinal"].to_i + existing_url = buildpack["buildpack"]["url"] + if existing_url == buildpack_url + error("The buildpack #{buildpack_url} is already set on your app.") + elsif ordinal == index + buildpack_url + else + existing_url + end end end - - # default behavior if index is out of range, or list is previously empty - # is to add buildpack to the list - if index < 0 or app_buildpacks.size <= index - buildpack_urls << buildpack_url - end - - update_buildpacks(buildpack_urls, "set") end # buildpacks:add BUILDPACK_URL @@ -86,31 +76,21 @@ def add error("Usage: heroku buildpacks:add BUILDPACK_URL.\nMust specify target buildpack URL.") end - validate_arguments! - index = (options[:index] || -1).to_i - index -= 1 - - app_buildpacks = api.get_app_buildpacks_v3(app)[:body] - - buildpack_urls = app_buildpacks.map { |buildpack| - ordinal = buildpack["ordinal"].to_i - existing_url = buildpack["buildpack"]["url"] - if existing_url == buildpack_url - error("The buildpack #{buildpack_url} is already set on your app.") - elsif ordinal == index - [buildpack_url, existing_url] - else - existing_url - end - }.flatten - - # default behavior if index is out of range, or list is previously empty - # is to add buildpack to the list - if index < 0 or app_buildpacks.size <= index - buildpack_urls << buildpack_url + index = get_index + + mutate_buildpacks(buildpack_url, index, "added") do |app_buildpacks| + app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"].to_i + existing_url = buildpack["buildpack"]["url"] + if existing_url == buildpack_url + error("The buildpack #{buildpack_url} is already set on your app.") + elsif ordinal == index + [buildpack_url, existing_url] + else + existing_url + end + }.flatten end - - update_buildpacks(buildpack_urls, "added") end # buildpacks:remove [BUILDPACK_URL] @@ -122,13 +102,13 @@ def add def remove if buildpack_url = shift_argument if options[:index] - error("Please choose either index or Buildpack URL, but not both, as arguments to this command.") + error("Please choose either index or Buildpack URL, but not both.") end else - validate_arguments! - index = options[:index].to_i - 1 + index = get_index end + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] if app_buildpacks.size == 0 @@ -172,6 +152,26 @@ def clear private + def mutate_buildpacks(buildpack_url, index, action) + app_buildpacks = api.get_app_buildpacks_v3(app)[:body] + + buildpack_urls = yield(app_buildpacks) + + # default behavior if index is out of range, or list is previously empty + # is to add buildpack to the list + if index < 0 or app_buildpacks.size <= index + buildpack_urls << buildpack_url + end + + update_buildpacks(buildpack_urls, action) + end + + def get_index(default=-1) + validate_arguments! + index = (options[:index] || default).to_i + index - 1 + end + def update_buildpacks(buildpack_urls, action) api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) display_buildpack_change(buildpack_urls, action) diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index 154e46c2d..8327d4405 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -414,7 +414,7 @@ def stub_get(*buildpacks) stderr, stdout = execute("buildpacks:remove -i 1 https://github.com/heroku/heroku-buildpack-java") expect(stdout).to eq("") expect(stderr).to eq <<-STDOUT - ! Please choose either index or Buildpack URL, but not both, as arguments to this command. + ! Please choose either index or Buildpack URL, but not both. STDOUT end From 65be55a203734b024a06daf8e2aee1515f6c3b5f Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 17:11:21 -0500 Subject: [PATCH 450/952] Extracted mutate_buildpacks_constructive method from common buildpacks mutating code --- lib/heroku/command/buildpacks.rb | 38 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 1a2ca2801..f45cd8a0e 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -46,7 +46,7 @@ def set index = get_index(1) - mutate_buildpacks(buildpack_url, index, "set") do |app_buildpacks| + mutate_buildpacks_constructive(buildpack_url, index, "set") do |app_buildpacks| app_buildpacks.map do |buildpack| ordinal = buildpack["ordinal"].to_i existing_url = buildpack["buildpack"]["url"] @@ -78,7 +78,7 @@ def add index = get_index - mutate_buildpacks(buildpack_url, index, "added") do |app_buildpacks| + mutate_buildpacks_constructive(buildpack_url, index, "added") do |app_buildpacks| app_buildpacks.map { |buildpack| ordinal = buildpack["ordinal"].to_i existing_url = buildpack["buildpack"]["url"] @@ -152,24 +152,40 @@ def clear private + def mutate_buildpacks_constructive(buildpack_url, index, action) + mutate_buildpacks(buildpack_url, index, action) do |app_buildpacks| + buildpack_urls = yield(app_buildpacks) + + # default behavior if index is out of range, or list is previously empty + # is to add buildpack to the list + if app_buildpacks.empty? or index.nil? or app_buildpacks.size < index + buildpack_urls << buildpack_url + end + + buildpack_urls + end + end + def mutate_buildpacks(buildpack_url, index, action) app_buildpacks = api.get_app_buildpacks_v3(app)[:body] buildpack_urls = yield(app_buildpacks) - # default behavior if index is out of range, or list is previously empty - # is to add buildpack to the list - if index < 0 or app_buildpacks.size <= index - buildpack_urls << buildpack_url - end - update_buildpacks(buildpack_urls, action) end - def get_index(default=-1) + def get_index(default=nil) validate_arguments! - index = (options[:index] || default).to_i - index - 1 + if options[:index] + index = options[:index].to_i + index -= 1 + if index < 0 + error("Invalid index. Must be greater than 0.") + end + index + else + default + end end def update_buildpacks(buildpack_urls, action) From 1d74b29d11229309b1605e2349f8437433aa5404 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 17:20:21 -0500 Subject: [PATCH 451/952] Refactored buildpacks output and structuring to reduce duplicate code --- lib/heroku/command/buildpacks.rb | 58 ++++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index f45cd8a0e..13b1c20c8 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -109,36 +109,36 @@ def remove end - app_buildpacks = api.get_app_buildpacks_v3(app)[:body] - - if app_buildpacks.size == 0 - error("No buildpacks were found. Next release on #{app} will detect buildpack normally.") - end + mutate_buildpacks(buildpack_url, index, "removed") do |app_buildpacks| + if app_buildpacks.size == 0 + error("No buildpacks were found. Next release on #{app} will detect buildpack normally.") + end - if index and (index < 0 or index > app_buildpacks.size) - if app_buildpacks.size == 1 - error("Invalid index. Only valid value is 1.") - else - error("Invalid index. Please choose a value between 1 and #{app_buildpacks.size}") + if index and (index < 0 or index > app_buildpacks.size) + if app_buildpacks.size == 1 + error("Invalid index. Only valid value is 1.") + else + error("Invalid index. Please choose a value between 1 and #{app_buildpacks.size}") + end end - end - buildpack_urls = app_buildpacks.map { |buildpack| - ordinal = buildpack["ordinal"].to_i - if ordinal == index - nil - elsif buildpack["buildpack"]["url"] == buildpack_url - nil - else - buildpack["buildpack"]["url"] + buildpack_urls = app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"].to_i + if ordinal == index + nil + elsif buildpack["buildpack"]["url"] == buildpack_url + nil + else + buildpack["buildpack"]["url"] + end + }.compact + + if buildpack_urls.size == app_buildpacks.size + error("Buildpack not found. Nothing was removed.") end - }.compact - if buildpack_urls.size == app_buildpacks.size - error("Buildpack not found. Nothing was removed.") + buildpack_urls end - - update_buildpacks(buildpack_urls, "removed") end # buildpacks:clear @@ -147,7 +147,7 @@ def remove # def clear api.put_app_buildpacks_v3(app, {:updates => []}) - display_no_buildpacks("s cleared") + display_no_buildpacks("cleared", true) end private @@ -216,16 +216,16 @@ def display_buildpack_change(buildpack_urls, action) end end - def display_no_buildpacks(action=" removed") + def display_no_buildpacks(action="removed", plural=false) vars = api.get_config_vars(app).body if vars.has_key?("BUILDPACK_URL") - display "Buildpack#{action}." + display "Buildpack#{plural ? "s" : ""} #{action}." warn "WARNING: The BUILDPACK_URL config var is still set and will be used for the next release" elsif vars.has_key?("LANGUAGE_PACK_URL") - display "Buildpack#{action}." + display "Buildpack#{plural ? "s" : ""} #{action}." warn "WARNING: The LANGUAGE_PACK_URL config var is still set and will be used for the next release" else - display "Buildpack#{action}. Next release on #{app} will detect buildpack normally." + display "Buildpack#{plural ? "s" : ""} #{action}. Next release on #{app} will detect buildpack normally." end end From 33d1ebf0646eabf3b2eb1eeb1fca9a0b5327b72e Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 17:26:24 -0500 Subject: [PATCH 452/952] Improved indentation of buildpacks output --- lib/heroku/command/buildpacks.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 13b1c20c8..0e30a35da 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -25,7 +25,7 @@ def index display("#{app} has no Buildpack URL set.") else styled_header("#{app} Buildpack URL#{app_buildpacks.size > 1 ? 's' : ''}") - display_buildpacks(app_buildpacks.map{|bp| bp["buildpack"]["url"]}) + display_buildpacks(app_buildpacks.map{|bp| bp["buildpack"]["url"]}, "") end end @@ -193,12 +193,12 @@ def update_buildpacks(buildpack_urls, action) display_buildpack_change(buildpack_urls, action) end - def display_buildpacks(buildpacks) + def display_buildpacks(buildpacks, indent=" ") if (buildpacks.size == 1) display(buildpacks.first) else buildpacks.each_with_index do |bp, i| - display(" #{i+1}. #{bp}") + display("#{indent}#{i+1}. #{bp}") end end end From 173f9a49d53ae006530ec93f72a4ba32ff904c43 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Mon, 13 Apr 2015 17:34:12 -0500 Subject: [PATCH 453/952] Improved buildpacks:set default when no index is provided, and added a test for index on multi-buildpacks --- lib/heroku/command/buildpacks.rb | 2 +- spec/heroku/command/buildpacks_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 0e30a35da..31c44b502 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -44,7 +44,7 @@ def set error("Usage: heroku buildpacks:set BUILDPACK_URL.\nMust specify target buildpack URL.") end - index = get_index(1) + index = get_index(0) mutate_buildpacks_constructive(buildpack_url, index, "set") do |app_buildpacks| app_buildpacks.map do |buildpack| diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index 8327d4405..36fddde48 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -67,6 +67,23 @@ def stub_get(*buildpacks) STDOUT end end + + context "with two buildpack URLs set" do + before(:each) do + Excon.stubs.shift + stub_get("https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-ruby") + end + + it "does not display a buildpack URL" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URLs +1. https://github.com/heroku/heroku-buildpack-java +2. https://github.com/heroku/heroku-buildpack-ruby + STDOUT + end + end end describe "set" do From fb1228066f4e6f211ce40367db179283435eab28 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Tue, 14 Apr 2015 09:55:53 -0500 Subject: [PATCH 454/952] Consolidated more duplicate code in buildpacks add and set commands --- lib/heroku/command/buildpacks.rb | 44 +++++++++++++++----------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 31c44b502..3df9c9cc1 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -46,17 +46,11 @@ def set index = get_index(0) - mutate_buildpacks_constructive(buildpack_url, index, "set") do |app_buildpacks| - app_buildpacks.map do |buildpack| - ordinal = buildpack["ordinal"].to_i - existing_url = buildpack["buildpack"]["url"] - if existing_url == buildpack_url - error("The buildpack #{buildpack_url} is already set on your app.") - elsif ordinal == index - buildpack_url - else - existing_url - end + mutate_buildpacks_constructive(buildpack_url, index, "set") do |existing_url, ordinal| + if ordinal == index + buildpack_url + else + existing_url end end end @@ -78,18 +72,12 @@ def add index = get_index - mutate_buildpacks_constructive(buildpack_url, index, "added") do |app_buildpacks| - app_buildpacks.map { |buildpack| - ordinal = buildpack["ordinal"].to_i - existing_url = buildpack["buildpack"]["url"] - if existing_url == buildpack_url - error("The buildpack #{buildpack_url} is already set on your app.") - elsif ordinal == index - [buildpack_url, existing_url] - else - existing_url - end - }.flatten + mutate_buildpacks_constructive(buildpack_url, index, "added") do |existing_url, ordinal| + if ordinal == index + [buildpack_url, existing_url] + else + existing_url + end end end @@ -154,7 +142,15 @@ def clear def mutate_buildpacks_constructive(buildpack_url, index, action) mutate_buildpacks(buildpack_url, index, action) do |app_buildpacks| - buildpack_urls = yield(app_buildpacks) + buildpack_urls = app_buildpacks.map { |buildpack| + ordinal = buildpack["ordinal"] + existing_url = buildpack["buildpack"]["url"] + if existing_url == buildpack_url + error("The buildpack #{buildpack_url} is already set on your app.") + else + yield(existing_url, ordinal) + end + }.flatten.compact # default behavior if index is out of range, or list is previously empty # is to add buildpack to the list From cafe98684cbc0dbdc14f16deb98dbed0c6413c3e Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Tue, 14 Apr 2015 10:54:24 -0700 Subject: [PATCH 455/952] add method that should have been added with cafe9ada0df64abec36d59da549bac952263beb9 --- lib/heroku/command/pg.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index bf5f0e9ac..e36ebf968 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -495,6 +495,10 @@ def display_db(name, db) display end + def in_maintenance?(app) + api.get_app_maintenance(app).body['maintenance'] + end + def hpg_client(attachment) Heroku::Client::HerokuPostgresql.new(attachment) end From 5e9811af14bdde5a375c883f337da1091633471f Mon Sep 17 00:00:00 2001 From: Daniel Farina Date: Thu, 16 Apr 2015 11:09:08 -0700 Subject: [PATCH 456/952] Add --verbose option to pg:ps This prints idle queries in addition to the active ones. Chad Bailey related that this causes confusion when there are connection spikes and exhaustion, typically resulting from connection leaks. In those cases, the backends are idle, and thus not reported, bewildering the user without convenient recourse. --- lib/heroku/command/pg.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index e36ebf968..1b1d4654f 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -241,6 +241,8 @@ def credentials # # view active queries with execution time # + # -v,--verbose # also show idle connections + # def ps requires_preauth sql = %Q( @@ -255,11 +257,15 @@ def ps WHERE #{query_column} <> '' #{ - if nine_two? - "AND state <> 'idle'" - else - "AND current_query <> ''" - end + # Apply idle-backend filter appropriate to versions and options. + case + when options[:verbose] + '' + when nine_two? + "AND state <> 'idle'" + else + "AND current_query <> ''" + end } AND #{pid_column} <> pg_backend_pid() ORDER BY query_start DESC From 0dc6d7c1a8204749e729fad4d94cbe4196ac1706 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Thu, 16 Apr 2015 16:20:45 -0500 Subject: [PATCH 457/952] Handle buildpacks:remove invalid argument cases better --- lib/heroku/command/buildpacks.rb | 5 +++-- spec/heroku/command/buildpacks_spec.rb | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 3df9c9cc1..19502d5b9 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -92,11 +92,12 @@ def remove if options[:index] error("Please choose either index or Buildpack URL, but not both.") end + elsif index = get_index + # cool! else - index = get_index + error("Usage: heroku buildpacks:remove [BUILDPACK_URL].\nMust specify a buildpack to remove, either by index or URL.") end - mutate_buildpacks(buildpack_url, index, "removed") do |app_buildpacks| if app_buildpacks.size == 0 error("No buildpacks were found. Next release on #{app} will detect buildpack normally.") diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index 36fddde48..3c202fda1 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -505,6 +505,15 @@ def stub_get(*buildpacks) ! Invalid index. Please choose a value between 1 and 2 STDOUT end + + it "checks if index or url is provided" do + stderr, stdout = execute("buildpacks:remove") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDOUT + ! Usage: heroku buildpacks:remove [BUILDPACK_URL]. + ! Must specify a buildpack to remove, either by index or URL. + STDOUT + end end end From fdd766a265f75c8918400d4ba437b6845568058c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 21 Apr 2015 10:52:53 -0700 Subject: [PATCH 458/952] added shell option for config:get --- lib/heroku/command/config.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index 3bc61d71a..9d85da579 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -99,6 +99,8 @@ def set # # display a config value for an app # + # -s, --shell # output config var in shell format + # #Examples: # # $ heroku config:get A @@ -112,7 +114,11 @@ def get vars = api.get_config_vars(app).body key, value = vars.detect {|k,v| k == key} - display(value.to_s) + if options[:shell] + display("#{key}=#{value}") + else + display(value.to_s) + end end # config:unset KEY1 [KEY2 ...] From b722aefeb87aa3929d55912fefad67e426f25db5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 21 Apr 2015 14:15:04 -0700 Subject: [PATCH 459/952] new fork implementation --- lib/heroku/api/releases_v3.rb | 34 ------- lib/heroku/command/fork.rb | 167 +------------------------------ spec/heroku/command/fork_spec.rb | 136 ------------------------- 3 files changed, 4 insertions(+), 333 deletions(-) delete mode 100644 lib/heroku/api/releases_v3.rb delete mode 100644 spec/heroku/command/fork_spec.rb diff --git a/lib/heroku/api/releases_v3.rb b/lib/heroku/api/releases_v3.rb deleted file mode 100644 index 632f168f7..000000000 --- a/lib/heroku/api/releases_v3.rb +++ /dev/null @@ -1,34 +0,0 @@ -module Heroku - class API - def get_releases_v3(app, range=nil) - headers = { 'Accept' => 'application/vnd.heroku+json; version=3' } - headers.merge!('Range' => range) if range - request( - :expects => [ 200, 206 ], - :headers => headers, - :method => :get, - :path => "/apps/#{app}/releases" - ) - end - - def post_release_v3(app, slug_id, opts={}) - headers = { - 'Accept' => 'application/vnd.heroku+json; version=3', - 'Content-Type' => 'application/json' - } - headers.merge!('Heroku-Deploy-Type' => opts[:deploy_type]) if opts[:deploy_type] - headers.merge!('Heroku-Deploy-Source' => opts[:deploy_source]) if opts[:deploy_source] - - body = { 'slug' => slug_id } - body.merge!('description' => opts[:description]) if opts[:description] - - request( - :expects => 201, - :headers => headers, - :method => :post, - :path => "/apps/#{app}/releases", - :body => Heroku::Helpers.json_encode(body) - ) - end - end -end diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index efe1a0155..d833bdd47 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -1,4 +1,3 @@ -require "heroku/api/releases_v3" require "heroku/command/base" module Heroku::Command @@ -14,170 +13,12 @@ class Fork < Base # # -s, --stack STACK # specify a stack for the new app # --region REGION # specify a region + # --skip-pg # skip postgres databases # def index - options[:ignore_no_org] = true - - from = app - to = shift_argument || "#{from}-#{(rand*1000).to_i}" - if from == to - raise Heroku::Command::CommandFailed.new("Cannot fork to the same app.") - end - - begin - api.get_app(to).body - error "#{to} app exists.\nUSAGE: heroku fork -a COPY_FROM COPY_TO" - rescue - end - from_info = api.get_app(from).body - - to_info = action("Creating fork #{to}", :org => !!org) do - params = { - "name" => to, - "region" => options[:region] || from_info["region"], - "stack" => options[:stack] || from_info["stack"], - "tier" => from_info["tier"] == "legacy" ? "production" : from_info["tier"] - } - - if org - org_api.post_app(params, org).body - else - api.post_app(params).body - end - end - - action("Copying slug") do - copy_slug(from_info, to_info) - end - - from_config = api.get_config_vars(from).body - from_addons = api.get_addons(from).body - - from_addons.each do |addon| - print "Adding #{addon["name"]}... " - begin - to_addon = api.post_addon(to, addon["name"]).body - puts "done" - rescue Heroku::API::Errors::RequestFailed => ex - puts "skipped (%s)" % json_decode(ex.response.body)["error"] - rescue Heroku::API::Errors::NotFound - puts "skipped (not found)" - end - if addon["name"] =~ /^heroku-postgresql:/ - from_var_name = "#{addon["attachment_name"]}_URL" - from_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - if from_config[from_var_name] == from_config["DATABASE_URL"] - from_config["DATABASE_URL"] = api.get_config_vars(to).body["#{from_attachment}_URL"] - end - from_config.delete(from_var_name) - - plan = addon["name"].split(":").last - unless %w(dev basic hobby-dev hobby-basic).include? plan - wait_for_db to, to_addon - end - - migrate_db addon, from, to_addon, to - end - end - - to_config = api.get_config_vars(to).body - - action("Copying config vars") do - diff = from_config.inject({}) do |ax, (key, val)| - ax[key] = val unless to_config[key] - ax - end - api.put_config_vars to, diff - end - - puts "Fork complete, view it at #{to_info['web_url']}" - rescue => e - raise if e.is_a?(Heroku::Command::CommandFailed) - - puts "Failed to fork app #{from} to #{to}." - message = "WARNING: Potentially Destructive Action\nThis command will destroy #{to} (including all add-ons)." - - if confirm_command(to, message) - action("Deleting #{to}") do - begin - api.delete_app(to) - rescue Heroku::API::Errors::NotFound - end - end - end - puts "Original exception below:" - raise e + Heroku::JSPlugin.setup + Heroku::JSPlugin.install('heroku-fork') unless Heroku::JSPlugin.is_plugin_installed?('heroku-fork') + Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) end - - private - - def copy_slug(from_info, to_info) - from = from_info["name"] - to = to_info["name"] - from_releases = api.get_releases_v3(from, 'version ..; order=desc,max=1;').body - raise Heroku::Command::CommandFailed.new("No releases on #{from}") if from_releases.empty? - from_slug = from_releases.first.fetch('slug', {}) - raise Heroku::Command::CommandFailed.new("No slug on #{from}") unless from_slug - api.post_release_v3(to, - from_slug["id"], - :description => "Forked from #{from}", - :deploy_type => "fork", - :deploy_source => from_info["id"]) - end - - def migrate_db(from_addon, from, to_addon, to) - transfer = nil - - action("Transferring database (this can take some time)") do - from_config = api.get_config_vars(from).body - from_attachment = from_addon["attachment_name"] - to_config = api.get_config_vars(to).body - to_attachment = to_addon["message"].match(/Attached as (\w+)_URL\n/)[1] - - resolver = Heroku::Helpers::HerokuPostgresql::Resolver.new(to, api) - attachment = resolver.resolve("#{to_attachment}_URL", nil) - pgb = Heroku::Client::HerokuPostgresql.new(attachment) - transfer = pgb.pg_copy( - from_attachment.gsub('HEROKU_POSTGRESQL_',''), - from_config["#{from_attachment}_URL"], - to_attachment.gsub('HEROKU_POSTGRESQL_',''), - to_config["#{to_attachment}_URL"]) - - hpg_app_client = Heroku::Client::HerokuPostgresqlApp.new(to) - begin - transfer = hpg_app_client.transfers_get(transfer[:uuid]) - sleep 1 - end until transfer[:finished_at] - if !transfer[:succeeded] - error "An error occurred and your transfer did not finish." - end - print " " - end - end - - def pg_api - require "rest_client" - host = "postgres-api.heroku.com" - RestClient::Resource.new "https://#{host}/client/v11/databases", Heroku::Auth.user, Heroku::Auth.password - end - - def wait_for_db(app, attachment) - attachments = api.get_attachments(app).body.inject({}) { |ax,att| ax.update(att["name"] => att["resource"]["name"]) } - attachment_name = attachment["message"].match(/Attached as (\w+)_URL\n/)[1] - action("Waiting for database to be ready (this can take some time)") do - loop do - begin - waiting = json_decode(pg_api["#{attachments[attachment_name]}/wait_status"].get.to_s)["waiting?"] - break unless waiting - sleep 5 - rescue RestClient::ResourceNotFound - rescue Interrupt - exit 0 - end - end - print " " - end - end - end end diff --git a/spec/heroku/command/fork_spec.rb b/spec/heroku/command/fork_spec.rb deleted file mode 100644 index 9cdf0bd21..000000000 --- a/spec/heroku/command/fork_spec.rb +++ /dev/null @@ -1,136 +0,0 @@ -require "heroku/api/releases_v3" -require "spec_helper" -require "heroku/command/fork" - -module Heroku::Command - - describe Fork do - - before(:each) do - stub_core - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - begin - api.delete_app("example-fork") - rescue Heroku::API::Errors::NotFound - end - end - - context "successfully" do - - before(:each) do - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [{"slug" => {"id" => "SLUG_ID"}}], - :status => 206}) - - Excon.stub({ :method => :post, - :path => "/apps/example-fork/releases"}, - { :status => 201}) - end - - after(:each) do - Excon.stubs.shift - Excon.stubs.shift - end - - it "forks an app" do - stderr, stdout = execute("fork example-fork") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Creating fork example-fork... done -Copying slug... done -Copying config vars... done -Fork complete, view it at http://example-fork.herokuapp.com/ -STDOUT - end - - it "copies slug" do - from_info = api.get_app("example").body - expect_any_instance_of(Heroku::API).to receive(:get_releases_v3).with("example", "version ..; order=desc,max=1;").and_call_original - expect_any_instance_of(Heroku::API).to receive(:post_release_v3).with("example-fork", - "SLUG_ID", - :description => "Forked from example", - :deploy_type => "fork", - :deploy_source => from_info["id"]).and_call_original - execute("fork example-fork") - end - - it "copies config vars" do - config_vars = { - "SECRET" => "imasecret", - "FOO" => "bar", - "LANG_ENV" => "production" - } - api.put_config_vars("example", config_vars) - execute("fork example-fork") - expect(api.get_config_vars("example-fork").body).to eq(config_vars) - end - - it "re-provisions add-ons" do - api.post_addon("example", "heroku-postgresql:hobby-dev") - execute("fork example-fork") - expect(api.get_addons("example-fork").body[0]["name"]).to eq("heroku-postgresql:hobby-dev") - end - end - - describe "error handling" do - it "fails if no source release exists" do - begin - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [], - :status => 206}) - execute("fork example-fork") - raise - rescue Heroku::Command::CommandFailed => e - expect(e.message).to eq("No releases on example") - ensure - Excon.stubs.shift - end - end - - it "fails if source slug does not exist" do - begin - Excon.stub({ :method => :get, - :path => "/apps/example/releases" }, - { :body => [{"slug" => nil}], - :status => 206}) - execute("fork example-fork") - raise - rescue Heroku::Command::CommandFailed => e - expect(e.message).to eq("No slug on example") - ensure - Excon.stubs.shift - end - end - - it "doesn't attempt to fork to the same app" do - expect do - execute("fork example") - end.to raise_error(Heroku::Command::CommandFailed, /same app/) - end - - it "confirms before deleting the app" do - Excon.stub({:path => "/apps/example/releases"}, {:status => 500}) - begin - execute("fork example-fork") - rescue Heroku::API::Errors::ErrorWithResponse - ensure - Excon.stubs.shift - end - expect(api.get_apps.body.map { |app| app["name"] }).to eq( - %w( example example-fork ) - ) - end - - it "deletes fork app on error, before re-raising" do - stub(Heroku::Command).confirm_command.returns(true) - expect(api.get_apps.body.map { |app| app["name"] }).to eq(%w( example )) - end - end - end -end From 886d46883ece671ee7270bf7d2d6e10ff657e5aa Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 21 Apr 2015 14:18:57 -0700 Subject: [PATCH 460/952] v3.32.0 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cd5738a26..a1871dc7e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.32.0 2015-04-21 +================= +Added new fork implementation +Added --verbose option to pg:ps +Fixed bug with pg in_maintenance? flag +Fixed issue with windows home folders and non-ascii characters +No longer exits if netrc is missing but HEROKU_API_KEY is provided + 3.31.3 2015-04-09 ================= Fixed some bugs around pg:backups diff --git a/Gemfile.lock b/Gemfile.lock index 667ea02fb..165d42401 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.31.3) + heroku (3.32.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4aacf1c2f..86a89df47 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.31.3" + VERSION = "3.32.0" end From 01280fc2875da94348afc67eb751aff49cf4066c Mon Sep 17 00:00:00 2001 From: omarkj Date: Wed, 22 Apr 2015 15:23:47 -0700 Subject: [PATCH 461/952] Show quota information. --- lib/heroku/command/ps.rb | 29 +++++++++++++++++++++++++++-- lib/heroku/helpers.rb | 9 +++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 4460a1df0..ad5d2033b 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -97,7 +97,32 @@ def workers # def index validate_arguments! - resp = api.request( + quota_resp = api.request( + :expects => [200, 404], + :method => :get, + :path => "/apps/#{app}/quota", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + } + ) + + if quota_resp.status = 200 + quota = quota_resp.body + now = Time.now.getutc + quota_message = if quota["allow_until"] + "Free quota left:" + elsif quota["deny_until"] + "Free quota exhausted. Unidle available in:" + end + if quota_message + quota_timestamp = (quota["allow_until"] ? Time.parse(quota["allow_until"]).getutc : Time.parse(quota["deny_until"]).getutc) + time_left = time_remaining(Time.now.getutc, quota_timestamp) + display("#{quota_message} #{time_left}") + end + end + + processes_resp = api.request( :expects => 200, :method => :get, :path => "/apps/#{app}/dynos", @@ -106,7 +131,7 @@ def index "Content-Type" => "application/json" } ) - processes = resp.body + processes = processes_resp.body processes_by_command = Hash.new {|hash,key| hash[key] = []} processes.each do |process| diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 07fabbe2d..393e3ad55 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -139,6 +139,15 @@ def time_ago(since) message end + def time_remaining(from, to) + secs = (to - from).to_i + mins = secs / 60 + hours = mins / 60 + return "#{hours}h #{mins % 60}m" if hours > 0 + return "#{mins}m #{secs % 60}s" if mins > 0 + return "#{secs}s" if secs >= 0 + end + def truncate(text, length) return "" if text.nil? if text.size > length From bbca25a19fc5a28d76004e55f9ad58f6bd19f7c4 Mon Sep 17 00:00:00 2001 From: omarkj Date: Wed, 22 Apr 2015 15:24:15 -0700 Subject: [PATCH 462/952] Add stubs to new 404 calls. --- spec/heroku/command/ps_spec.rb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index e7bd38424..9606adcf7 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -44,6 +44,10 @@ end.to_json, :status => 200 ) + Excon.stub( + { :method => :get, :path => "/apps/example/quota" }, + :status => 404 + ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(10).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") expect(stderr).to eq("") @@ -80,6 +84,10 @@ end.to_json, :status => 200 ) + Excon.stub( + { :method => :get, :path => "/apps/example/quota" }, + :status => 404 + ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return('2012/09/11 12:34:56 (~ 0s ago)') stderr, stdout = execute("ps") expect(stderr).to eq("") @@ -108,6 +116,10 @@ end.to_json, :status => 200 ) + Excon.stub( + { :method => :get, :path => "/apps/example/quota" }, + :status => 404 + ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") @@ -137,6 +149,10 @@ end.to_json, :status => 200 ) + Excon.stub( + { :method => :get, :path => "/apps/example/quota" }, + :status => 404 + ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") @@ -167,6 +183,10 @@ end.to_json, :status => 200 ) + Excon.stub( + { :method => :get, :path => "/apps/example/quota" }, + :status => 404 + ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(4).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") stderr, stdout = execute("ps") expect(stderr).to eq("") From 18c1768ad2ba6b42e8ae342aa47e9f36cea331ea Mon Sep 17 00:00:00 2001 From: omarkj Date: Wed, 22 Apr 2015 15:53:51 -0700 Subject: [PATCH 463/952] Add specs for new quota tests --- spec/heroku/command/ps_spec.rb | 39 ++++++++++++++++++++++++++++++++++ spec/heroku/helpers_spec.rb | 20 +++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index 9606adcf7..a84876040 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -203,6 +203,45 @@ end + it "displays how much run-time is left if the application has quota (seconds)" do + allow_until = (Time.now + 30).getutc + Excon.stub( + { :method => :get, :path => "/apps/example/dynos" }, + :body => 1.times.map do |i| + { + "size" => "1X", + "updated_at" => "2012-09-11T12:34:56Z", + "command" => "bundle exec thin start -p $PORT", + "created_at" => "2012-09-11T12:30:56Z", + "id" => "a94d0fa2-8509-4dab-8742-be7bfe768ecc", + "name" => "web.#{i+1}", + "state" => "up", + "type" => "web" + } + end.to_json, + :status => 200 + ) + Excon.stub( + { :method => :get, :path => "/apps/example/quota" }, + :body => + { + "allow_until" => allow_until.iso8601, + "deny_until" => nil, + }.to_json, + :status => 404 + ) + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).once.times.and_return("2012/09/11 12:34:56 (~ 0s ago)") + expect_any_instance_of(Heroku::Command::Ps).to receive(:time_remaining).and_return("20s") + stderr, stdout = execute("ps") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Free quota left: 20s +=== web (1X): `bundle exec thin start -p $PORT` +web.1: up 2012/09/11 12:34:56 (~ 0s ago) + +STDOUT + end + describe "ps:restart" do it "restarts all dynos with no args" do diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index 9dfb8dd60..62ae06a5b 100644 --- a/spec/heroku/helpers_spec.rb +++ b/spec/heroku/helpers_spec.rb @@ -5,6 +5,26 @@ module Heroku describe Helpers do include Heroku::Helpers + context "time_remaining" do + it "should display seconds remaining correctly" do + now = Time.now + future = Time.now + 30 + expect(time_remaining(now, future)).to eq("30s") + end + + it "should display minutes remaining correctly" do + now = Time.now + future = Time.now + 65 + expect(time_remaining(now, future)).to eq("1m 5s") + end + + it "should display hours remaining correctly" do + now = Time.now + future = Time.now + (70*60) + expect(time_remaining(now, future)).to eq("1h 10m") + end + end + context "display_object" do it "should display Array correctly" do From 633fe1745f00c6fe5b40ea7e5b99d34300d2c735 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 24 Apr 2015 18:46:18 +0000 Subject: [PATCH 464/952] added passing specs for pg:backups command --- spec/heroku/command/pg_backups_spec.rb | 76 ++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 38ea07d91..a3b139b99 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -122,6 +122,82 @@ module Heroku::Command end end + describe "heroku pg:backups" do + let(:logged_at) { Time.now } + let(:started_at) { Time.now } + let(:finished_at) { Time.now } + let(:from_name) { 'RED' } + let(:source_size) { 42 } + let(:backup_size) { source_size / 2 } + + let(:logs) { [{ 'created_at' => logged_at, 'message' => "hello world" }] } + let(:transfers) do + [ + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 1, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_name => from_name, :to_name => 'BACKUP', + :num => 2, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :succeeded => false }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 3, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 4, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => false }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'pg_dump', :to_type => 'pg_restore', num: 5, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'pg_dump', :to_type => 'pg_restore', num: 6, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => false } + ] + end + + before do + (1..3).each do |n| + stub_pgapp.transfers_get(n, true). + returns(transfers.find { |xfer| xfer[:num] == n }) + end + stub_pgapp.transfers.returns(transfers) + end + + it "lists successful backups" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/b001\s*Finished/) + end + + it "list failed backups" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/b002\s*Failed/) + end + + it "lists successful restores" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r003\s*Finished/) + end + + it "lists failed restores" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r004\s*Failed/) + end + + it "lists successful copies" + it "lists failed copies" + end + describe "heroku pg:backups info" do let(:logged_at) { Time.now } let(:started_at) { Time.now } From 8c442c03760326d87fc72a59e8caccf1fa6a6463 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 24 Apr 2015 19:02:22 +0000 Subject: [PATCH 465/952] added pg:copy display to pg:backups command output --- .ruby-version | 1 + lib/heroku/command/pg_backups.rb | 23 +++++++++++++++++++++++ spec/heroku/command/pg_backups_spec.rb | 14 ++++++++++++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 000000000..ccbccc3dc --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.2.0 diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 2e08f283c..57f09db0f 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -208,6 +208,29 @@ def list_backups ["ID", "Restore Time", "Status", "Size", "Database"] ) end + + display "\n=== Copies" + display_restores = transfers.select do |r| + r[:from_type] == 'pg_dump' && r[:to_type] == 'pg_restore' + end.sort_by { |r| r[:created_at] }.reverse.map do |r| + { + "id" => transfer_name(r), + "created_at" => r[:created_at], + "status" => transfer_status(r), + "size" => size_pretty(r[:processed_bytes]), + "to_database" => r[:to_name] || 'UNKNOWN', + "from_database" => r[:from_name] || 'UNKNOWN' + } + end + if display_restores.empty? + error("No copies found. Use `heroku pg:copy` to copy a database to another") + else + display_table( + display_restores, + %w(id created_at status size from_database to_database), + ["ID", "Restore Time", "Status", "Size", "From Database", "To Database"] + ) + end end def backup_status diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index a3b139b99..cab312d9c 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -158,10 +158,12 @@ module Heroku::Command { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', :from_type => 'pg_dump', :to_type => 'pg_restore', num: 5, :started_at => Time.now, :finished_at => Time.now, + :from_name => "CRIMSON", :to_name => "CLOVER", :processed_bytes => 42, :succeeded => true }, { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', :from_type => 'pg_dump', :to_type => 'pg_restore', num: 6, :started_at => Time.now, :finished_at => Time.now, + :from_name => "CRIMSON", :to_name => "CLOVER", :processed_bytes => 42, :succeeded => false } ] end @@ -194,8 +196,16 @@ module Heroku::Command expect(stdout).to match(/r004\s*Failed/) end - it "lists successful copies" - it "lists failed copies" + it "lists successful copies" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/===\sCopies/) + expect(stdout).to match(/c005\s*Finished/) + end + + it "lists failed copies" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/c006\s*Failed/) + end end describe "heroku pg:backups info" do From 0efb830f863185b52a7eba3e55538a1e578f20d1 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 24 Apr 2015 19:16:12 +0000 Subject: [PATCH 466/952] refactored pg:backups info tests inside pg:backups tests --- spec/heroku/command/pg_backups_spec.rb | 151 ++++++++++--------------- 1 file changed, 61 insertions(+), 90 deletions(-) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index cab312d9c..7e00fb3d6 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -164,12 +164,20 @@ module Heroku::Command :from_type => 'pg_dump', :to_type => 'pg_restore', num: 6, :started_at => Time.now, :finished_at => Time.now, :from_name => "CRIMSON", :to_name => "CLOVER", - :processed_bytes => 42, :succeeded => false } + :processed_bytes => 42, :succeeded => false }, + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', + :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', + :num => 7, :logs => logs, + :from_type => 'pg_dump', :to_type => 'gof3r', + :started_at => started_at, :finished_at => finished_at, + :processed_bytes => backup_size, :source_bytes => source_size, + :options => { "pgbackups_name" => "b047" }, + :succeeded => true } ] end before do - (1..3).each do |n| + (1..7).each do |n| stub_pgapp.transfers_get(n, true). returns(transfers.find { |xfer| xfer[:num] == n }) end @@ -186,6 +194,11 @@ module Heroku::Command expect(stdout).to match(/b002\s*Failed/) end + it "lists old pgbackups" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/ob047\s*Finished/) + end + it "lists successful restores" do stderr, stdout = execute("pg:backups") expect(stdout).to match(/r003\s*Finished/) @@ -200,62 +213,18 @@ module Heroku::Command stderr, stdout = execute("pg:backups") expect(stdout).to match(/===\sCopies/) expect(stdout).to match(/c005\s*Finished/) - end + end it "lists failed copies" do stderr, stdout = execute("pg:backups") expect(stdout).to match(/c006\s*Failed/) end - end - - describe "heroku pg:backups info" do - let(:logged_at) { Time.now } - let(:started_at) { Time.now } - let(:finished_at) { Time.now } - let(:from_name) { 'RED' } - let(:source_size) { 42 } - let(:backup_size) { source_size / 2 } - let(:logs) { [{ 'created_at' => logged_at, 'message' => "hello world" }] } - let(:transfers) do - [ - { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', - :from_name => from_name, :to_name => 'BACKUP', - :num => 1, :logs => logs, - :from_type => 'pg_dump', :to_type => 'gof3r', - :started_at => started_at, :finished_at => finished_at, - :processed_bytes => backup_size, :source_bytes => source_size, - :succeeded => true }, - { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', - :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', - :num => 2, :logs => logs, - :from_type => 'pg_dump', :to_type => 'gof3r', - :started_at => started_at, :finished_at => finished_at, - :processed_bytes => backup_size, :source_bytes => source_size, - :options => { "pgbackups_name" => "b047" }, - :succeeded => true }, - { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffe', - :from_name => from_name, :to_name => 'BACKUP', - :num => 3, :logs => logs, - :from_type => 'pg_dump', :to_type => 'gof3r', - :started_at => started_at, :finished_at => finished_at, - :processed_bytes => backup_size, :source_bytes => source_size, - :succeeded => true } - ] - end - - before do - (1..3).each do |n| - stub_pgapp.transfers_get(n, true). - returns(transfers.find { |xfer| xfer[:num] == n }) - end - stub_pgapp.transfers.returns(transfers) - end - - it "displays info for the given backup" do - stderr, stdout = execute("pg:backups info b001") - expect(stderr).to be_empty - expect(stdout).to eq <<-EOF + describe "heroku pg:backups info" do + it "displays info for the given backup" do + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF === Backup info: b001 Database: #{from_name} Started: #{started_at} @@ -266,13 +235,13 @@ module Heroku::Command Backup Size: #{backup_size}.0B (50% compression) === Backup Logs #{logged_at}: hello world - EOF - end + EOF + end - it "displays info for legacy PGBackups backups" do - stderr, stdout = execute("pg:backups info ob047") - expect(stderr).to be_empty - expect(stdout).to eq <<-EOF + it "displays info for legacy PGBackups backups" do + stderr, stdout = execute("pg:backups info ob047") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF === Backup info: ob047 Database: #{from_name} Started: #{started_at} @@ -283,14 +252,14 @@ module Heroku::Command Backup Size: #{backup_size}.0B (50% compression) === Backup Logs #{logged_at}: hello world - EOF - end + EOF + end - it "defaults to the latest backup if none is specified" do - stderr, stdout = execute("pg:backups info") - expect(stderr).to be_empty - expect(stdout).to eq <<-EOF -=== Backup info: b003 + it "defaults to the latest backup if none is specified" do + stderr, stdout = execute("pg:backups info") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF +=== Backup info: ob047 Database: #{from_name} Started: #{started_at} Finished: #{finished_at} @@ -300,15 +269,15 @@ module Heroku::Command Backup Size: #{backup_size}.0B (50% compression) === Backup Logs #{logged_at}: hello world - EOF - end + EOF + end - it "does not display finished time or compression ratio if backup is not finished" do - xfer = transfers.find { |xfer| xfer[:num] == 1 } - xfer[:finished_at] = nil - stderr, stdout = execute("pg:backups info b001") - expect(stderr).to be_empty - expect(stdout).to eq <<-EOF + it "does not display finished time or compression ratio if backup is not finished" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:finished_at] = nil + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF === Backup info: b001 Database: #{from_name} Started: #{started_at} @@ -318,15 +287,15 @@ module Heroku::Command Backup Size: #{backup_size}.0B === Backup Logs #{logged_at}: hello world - EOF - end + EOF + end - it "works when the progress is at 0 bytes" do - xfer = transfers.find { |xfer| xfer[:num] == 1 } - xfer[:processed_bytes] = 0 - stderr, stdout = execute("pg:backups info b001") - expect(stderr).to be_empty - expect(stdout).to eq <<-EOF + it "works when the progress is at 0 bytes" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:processed_bytes] = 0 + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF === Backup info: b001 Database: #{from_name} Started: #{started_at} @@ -337,15 +306,15 @@ module Heroku::Command Backup Size: 0.00B (0% compression) === Backup Logs #{logged_at}: hello world - EOF - end + EOF + end - it "works when the source size is 0 bytes" do - xfer = transfers.find { |xfer| xfer[:num] == 1 } - xfer[:source_bytes] = 0 - stderr, stdout = execute("pg:backups info b001") - expect(stderr).to be_empty - expect(stdout).to eq <<-EOF + it "works when the source size is 0 bytes" do + xfer = transfers.find { |xfer| xfer[:num] == 1 } + xfer[:source_bytes] = 0 + stderr, stdout = execute("pg:backups info b001") + expect(stderr).to be_empty + expect(stdout).to eq <<-EOF === Backup info: b001 Database: #{from_name} Started: #{started_at} @@ -355,10 +324,12 @@ module Heroku::Command Backup Size: #{backup_size}.0B === Backup Logs #{logged_at}: hello world - EOF + EOF + end end end + describe "heroku pg:backups restore" do let(:started_at) { Time.parse('2001-01-01 00:00:00') } let(:finished_at_1) { Time.parse('2001-01-01 01:00:00') } From 24a87506a1088a289480fcca7e04bea3a5fe67ff Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 24 Apr 2015 20:23:53 +0000 Subject: [PATCH 467/952] bugfixes --- lib/heroku/command/pg_backups.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 57f09db0f..7613601e2 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -178,7 +178,7 @@ def list_backups } end if display_backups.empty? - error("No backups. Capture one with `heroku pg:backups capture`.") + display("No backups. Capture one with `heroku pg:backups capture`.") else display_table( display_backups, @@ -189,7 +189,7 @@ def list_backups display "\n=== Restores" display_restores = transfers.select do |r| - r[:from_type] == 'gof3r' && r[:to_type] == 'pg_restore' + r[:from_type] != 'pg_dump' && r[:to_type] == 'pg_restore' end.sort_by { |r| r[:created_at] }.reverse.map do |r| { "id" => transfer_name(r), @@ -200,7 +200,7 @@ def list_backups } end if display_restores.empty? - error("No restores found. Use `heroku pg:backups restore` to restore a backup") + display("No restores found. Use `heroku pg:backups restore` to restore a backup") else display_table( display_restores, @@ -223,7 +223,7 @@ def list_backups } end if display_restores.empty? - error("No copies found. Use `heroku pg:copy` to copy a database to another") + display("No copies found. Use `heroku pg:copy` to copy a database to another") else display_table( display_restores, From 01cea92b082c34b1bd25114f9a663e38e3c1052e Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Fri, 24 Apr 2015 20:37:13 +0000 Subject: [PATCH 468/952] removed refs to app logs in transfer error output --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 7613601e2..c90ee597b 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -422,7 +422,7 @@ def poll_transfer(action, transfer_id) redisplay <<-EOF An error occurred and your backup did not finish. -Please run `heroku logs --ps pg-backups` for details. +Please run `heroku pg:backups info #{transfer_name(backup)}` for details. EOF end From ab169b2b5616fe0a90e81abf4c8e8f184b98c067 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Tue, 28 Apr 2015 14:51:39 +0000 Subject: [PATCH 469/952] removed errant ruby-version --- .ruby-version | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index ccbccc3dc..000000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -2.2.0 From 34aabf84ae5571522a6a2290283189a5878b68f6 Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Tue, 28 Apr 2015 15:28:03 +0000 Subject: [PATCH 470/952] remove ruby-version; limited restores and copies --- lib/heroku/command/pg_backups.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index c90ee597b..5020af39d 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -190,7 +190,7 @@ def list_backups display "\n=== Restores" display_restores = transfers.select do |r| r[:from_type] != 'pg_dump' && r[:to_type] == 'pg_restore' - end.sort_by { |r| r[:created_at] }.reverse.map do |r| + end.sort_by { |r| r[:created_at] }.reverse.first(20).map do |r| { "id" => transfer_name(r), "created_at" => r[:created_at], @@ -212,7 +212,7 @@ def list_backups display "\n=== Copies" display_restores = transfers.select do |r| r[:from_type] == 'pg_dump' && r[:to_type] == 'pg_restore' - end.sort_by { |r| r[:created_at] }.reverse.map do |r| + end.sort_by { |r| r[:created_at] }.reverse.first(20).map do |r| { "id" => transfer_name(r), "created_at" => r[:created_at], From 4998556aea3ad72d21277e6eb46d73978f35518c Mon Sep 17 00:00:00 2001 From: Chad Bailey Date: Tue, 28 Apr 2015 16:14:52 +0000 Subject: [PATCH 471/952] reduced to 10 each --- lib/heroku/command/pg_backups.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 5020af39d..b3f08e461 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -190,7 +190,7 @@ def list_backups display "\n=== Restores" display_restores = transfers.select do |r| r[:from_type] != 'pg_dump' && r[:to_type] == 'pg_restore' - end.sort_by { |r| r[:created_at] }.reverse.first(20).map do |r| + end.sort_by { |r| r[:created_at] }.reverse.first(10).map do |r| { "id" => transfer_name(r), "created_at" => r[:created_at], @@ -212,7 +212,7 @@ def list_backups display "\n=== Copies" display_restores = transfers.select do |r| r[:from_type] == 'pg_dump' && r[:to_type] == 'pg_restore' - end.sort_by { |r| r[:created_at] }.reverse.first(20).map do |r| + end.sort_by { |r| r[:created_at] }.reverse.first(10).map do |r| { "id" => transfer_name(r), "created_at" => r[:created_at], From 0baf883106da8301af0f73a6e327024511210d79 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 29 Apr 2015 10:29:43 -0700 Subject: [PATCH 472/952] only escape shell words if it is a tty --- lib/heroku/command/config.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index 9d85da579..d90142e78 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -42,7 +42,8 @@ def index vars.each {|key, value| vars[key] = value.to_s} if options[:shell] vars.keys.sort.each do |key| - display(%{#{key}=#{Shellwords.shellescape vars[key]}}) + out = $stdout.tty? ? Shellwords.shellescape(vars[key]) : vars[key] + display(%{#{key}=#{out}}) end else styled_header("#{app} Config Vars") From af7ef228a76e39035afe7d89778bc347345deeba Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 29 Apr 2015 10:39:31 -0700 Subject: [PATCH 473/952] escape config:get with shell escaping --- lib/heroku/command/config.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index d90142e78..0c2d671aa 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -115,8 +115,9 @@ def get vars = api.get_config_vars(app).body key, value = vars.detect {|k,v| k == key} - if options[:shell] - display("#{key}=#{value}") + if options[:shell] && value + out = $stdout.tty? ? Shellwords.shellescape(value) : value + display("#{key}=#{out}") else display(value.to_s) end From cafe839fac796e41f44ff5a680c79da4e54a7254 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Wed, 29 Apr 2015 17:04:14 -0700 Subject: [PATCH 474/952] import the dyno-types plugin --- lib/heroku/command/ps.rb | 166 ++++++++++++++++++++++++++++----- spec/heroku/command/ps_spec.rb | 12 +-- 2 files changed, 149 insertions(+), 29 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 4460a1df0..8ab94fdd1 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -1,12 +1,25 @@ +require "json" require "heroku/command/base" # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - PRICES = { - "P" => 0.8, - "PX" => 0.8, - } + PROCESS_TIERS = JSON.parse < :patch, + :path => "/apps/#{app}", + :body => json_encode("process_tier" => process_tier), + :headers => { + "Accept" => "application/vnd.heroku+json; version=edge", + "Content-Type" => "application/json" + } + ) + end + + def display_dyno_type_and_costs(app_resp, formation_resp) + tier_info = PROCESS_TIERS.detect { |t| t["tier"] == app_resp.body["process_tier"] } + + formation = formation_resp.body.reject {|ps| ps['quantity'] < 1} + + annotated = formation.sort_by{|d| d['type']}.map do |dyno| + cost = tier_info["cost"][dyno["size"]] * dyno["quantity"] / 100 + { + 'dyno' => dyno['type'], + 'type' => dyno['size'].rjust(4), + 'qty' => dyno['quantity'].to_s.rjust(3), + 'cost/mo' => cost.to_s.rjust(7) + } + end + + # in case of an app not yet released + annotated = [tier_info] if annotated.empty? + + display_table(annotated, annotated.first.keys, annotated.first.keys) + end + + def edge_app_info + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}", + :headers => { + "Accept" => "application/vnd.heroku+json; version=edge", + "Content-Type" => "application/json" + } + ) + end + + def edge_app_formation + api.request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}/formation", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + } + ) + end + + def special_case_change_tier_and_resize(type) + patch_tier("production") + override_args = edge_app_formation.body.map { |ps| "#{ps['type']}=#{type}" } + _original_resize(override_args) + end + + def _original_resize(override_args=nil) app change_map = {} - changes = args.map do |arg| - if arg =~ /^([a-zA-Z0-9_]+)=(\w+)$/ + changes = (override_args || args).map do |arg| + if arg =~ /^([a-zA-Z0-9_]+)=([\w-]+)$/ change_map[$1] = $2 { "process" => $1, "size" => $2 } end @@ -286,8 +408,8 @@ def resize if changes.empty? message = [ - "Usage: heroku ps:resize DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...]", - "Must specify DYNO and SIZE to resize." + "Usage: heroku dyno:type DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...]", + "Must specify DYNO and TYPE to resize." ] error(message.join("\n")) end @@ -308,14 +430,12 @@ def resize resp.body.select {|p| change_map.key?(p['type']) }.each do |p| size = p["size"] - price = if size.to_i > 0 - sprintf("%.2f", 0.05 * size.to_i) - else - sprintf("%.2f", PRICES[size]) - end - display "#{p["type"]} dynos now #{size} ($#{price}/dyno-hour)" + display "#{p["type"]} dynos now #{size} ($#{COSTS[size]}/month)" end end +end - alias_command "resize", "ps:resize" +%w[type restart scale stop].each do |cmd| + Heroku::Command::Base.alias_command "dyno:#{cmd}", "ps:#{cmd}" end + diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index e7bd38424..e1b23b834 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -314,7 +314,7 @@ expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done -web dynos now 2X ($0.10/dyno-hour) +web dynos now 2X ($72/month) STDOUT end @@ -330,7 +330,7 @@ }.to_json }, :body => [ - {"quantity" => 2, "size" => "4X", "type" => "web"}, + {"quantity" => 2, "size" => "1X", "type" => "web"}, {"quantity" => 1, "size" => "2X", "type" => "worker"} ], :status => 200 @@ -339,8 +339,8 @@ expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done -web dynos now 4X ($0.20/dyno-hour) -worker dynos now 2X ($0.10/dyno-hour) +web dynos now 1X ($36/month) +worker dynos now 2X ($72/month) STDOUT end @@ -365,8 +365,8 @@ expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Resizing and restarting the specified dynos... done -web dynos now PX ($0.80/dyno-hour) -worker dynos now PX ($0.80/dyno-hour) +web dynos now PX ($576/month) +worker dynos now PX ($576/month) STDOUT end From cafedab72a739eab7e750cc9339bf265bf572e31 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Wed, 29 Apr 2015 17:21:18 -0700 Subject: [PATCH 475/952] get rid of the json part --- lib/heroku/command/ps.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 8ab94fdd1..9c5eff0e5 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -1,17 +1,14 @@ -require "json" require "heroku/command/base" # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - PROCESS_TIERS = JSON.parse <"free", "max_scale"=>1, "max_processes"=>2, "cost"=>{"Free"=>0}}, + {"tier"=>"hobby", "max_scale"=>1, "max_processes"=>nil, "cost"=>{"Hobby"=>700}}, + {"tier"=>"production", "max_scale"=>100, "max_processes"=>nil, "cost"=>{"Standard-1X"=>2500, "Standard-2X"=>5000, "Performance"=>50000}}, + {"tier"=>"traditional", "max_scale"=>100, "max_processes"=>nil, "cost"=>{"1X"=>3600, "2X"=>7200, "PX"=>57600}} + ] costs = PROCESS_TIERS.collect do |tier| tier["cost"].collect do |name, cents_per_month| From cafed9d9b919d0d794c68b5f550af54906f1c987 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Wed, 29 Apr 2015 17:23:27 -0700 Subject: [PATCH 476/952] add heroku-dyno-types to the deprecated list, and sort it --- lib/heroku/plugin.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index e5fe7ce5c..c040ca911 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -12,10 +12,13 @@ class ErrorUpdatingSymlinkPlugin < StandardError; end heroku-certs heroku-credentials heroku-dyno-size + heroku-dyno-types + heroku-fork heroku-kill heroku-labs heroku-logging heroku-netrc + heroku-orgs heroku-pgdumps heroku-postgresql heroku-push @@ -29,8 +32,6 @@ class ErrorUpdatingSymlinkPlugin < StandardError; end heroku-two-factor pgbackups-automate pgcmd - heroku-fork - heroku-orgs ) attr_reader :name, :uri From d66823f7de7a2b6e20aca17b54a6d4ac1a54d773 Mon Sep 17 00:00:00 2001 From: Cyril David Date: Thu, 30 Apr 2015 10:55:21 -0700 Subject: [PATCH 477/952] Accept cedar-10 in create --stack via @tt --- lib/heroku/command/apps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index d926ba1b3..c21881216 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -232,7 +232,7 @@ def create params = { "name" => name, "region" => options[:region], - "stack" => options[:stack], + "stack" => Heroku::Command::Stack::Codex.in(options[:stack]), "locked" => options[:locked] } From 701dd0745eab8877ad7fc2755852e4d70c1c0cea Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 30 Apr 2015 16:01:37 -0700 Subject: [PATCH 478/952] error if no certificate is returned --- lib/heroku/command/certs.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 412e9e116..19ee48dfa 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -114,8 +114,12 @@ def info heroku.ssl_endpoint_info(app, cname) end - display "Certificate details:" - display_certificate_info(endpoint) + if endpoint + display "Certificate details:" + display_certificate_info(endpoint) + else + error "No certificate found." + end end # certs:remove From edaa3c62ef5e4f7ef6adf25c32bfc9ed0b12f1d1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 30 Apr 2015 16:04:56 -0700 Subject: [PATCH 479/952] whitespace cleanup and removed unused variable --- lib/heroku/command/certs.rb | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 19ee48dfa..1046089a6 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -59,7 +59,7 @@ def chain # The first key that signs the certificate will be printed back. # def key - crt, key = read_crt_and_key_through_ssl_doctor("Testing for signing key") + _, key = read_crt_and_key_through_ssl_doctor("Testing for signing key") puts key rescue UsageError fail("Usage: heroku certs:key CRT KEY [KEY ...]\nMust specify one certificate file and at least one key file.") @@ -157,14 +157,14 @@ def rollback display "New active certificate details:" display_certificate_info(endpoint) end - + # certs:generate DOMAIN - # - # Generate a key and certificate signing request (or self-signed certificate) - # for an app. Prompts for information to put in the certificate unless --now - # is used, or at least one of the --subject, --owner, --country, --area, or + # + # Generate a key and certificate signing request (or self-signed certificate) + # for an app. Prompts for information to put in the certificate unless --now + # is used, or at least one of the --subject, --owner, --country, --area, or # --city options is specified. - # + # # --selfsigned # generate a self-signed certificate instead of a CSR # --keysize BITSIZE # RSA key size in bits (default: 2048) # --owner NAME # name of organization certificate belongs to @@ -175,23 +175,23 @@ def rollback # --now # do not prompt for any owner information def generate request = Heroku::OpenSSL::CertificateRequest.new - + request.domain = args[0] || error("certs:generate must specify a domain") request.subject = cert_subject_for_domain_and_options(request.domain, options) request.self_signed = options[:selfsigned] || false request.key_size = (options[:keysize] || request.key_size).to_i - + result = request.generate - + explain_step_after_generate result - + rescue Heroku::OpenSSL::NotInstalledError => ex error("The OpenSSL command-line tools must be installed to use certs:generate.\n" + ex.installation_hint) - + rescue Heroku::OpenSSL::GenericError => ex error(ex.message) end - + private def current_endpoint @@ -238,7 +238,7 @@ def post_to_ssl_doctor(path, action_text = nil) begin certbody=File.read(arg) rescue => e - error("Unable to read #{arg} file: #{e}") + error("Unable to read #{arg} file: #{e}") end certbody }.join("\n") @@ -268,28 +268,28 @@ def read_crt_and_key_bypassing_ssl_doctor def read_crt_and_key options[:bypass] ? read_crt_and_key_bypassing_ssl_doctor : read_crt_and_key_through_ssl_doctor end - + def all_endpoint_domains endpoints = heroku.ssl_endpoint_list(app) endpoints.select { |endpoint| endpoint['ssl_cert'] && endpoint['ssl_cert']['cert_domains'] } \ .map { |endpoint| endpoint['ssl_cert']['cert_domains'] } \ .reduce(:+) end - + def prompt(question) display("#{question}: ", false) ask end - + def val_empty?(val) val.nil? or val.empty? end - + def cert_subject_for_domain_and_options(domain, options = {}) raise ArgumentError, "domain cannot be empty" if domain.nil? || domain.empty? - + subject, country, area, city, owner, now = options.values_at(:subject, :country, :area, :city, :owner, :now) - + if val_empty? subject if !now && [country, area, city, owner].all? { |v| val_empty? v } owner = prompt "Owner of this certificate" @@ -297,19 +297,19 @@ def cert_subject_for_domain_and_options(domain, options = {}) area = prompt "State/province/etc. of owner" city = prompt "City of owner" end - + subject = "" subject += "/C=#{country}" unless val_empty? country subject += "/ST=#{area}" unless val_empty? area subject += "/L=#{city}" unless val_empty? city subject += "/O=#{owner}" unless val_empty? owner - + subject += "/CN=#{domain}" end - + subject end - + def explain_step_after_generate(result) if result.csr_file.nil? display "Your key and self-signed certificate have been generated." @@ -319,7 +319,7 @@ def explain_step_after_generate(result) display "Submit the CSR in '#{result.csr_file}' to your preferred certificate authority." display "When you've received your certificate, run:" end - + needs_addon = false command = "add" begin @@ -327,7 +327,7 @@ def explain_step_after_generate(result) rescue RestClient::Forbidden needs_addon = true end - + display "$ heroku addons:add ssl:endpoint" if needs_addon display "$ heroku certs:#{command} #{result.crt_file || "CERTFILE"} #{result.key_file}" end From f9bbb46b2e4604501385d487ff2d1f87c5eb016f Mon Sep 17 00:00:00 2001 From: omarkj Date: Thu, 30 Apr 2015 17:35:29 -0700 Subject: [PATCH 480/952] Updated as per new API design. --- lib/heroku/command/ps.rb | 6 +++--- spec/heroku/command/ps_spec.rb | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index ad5d2033b..454a3e6c3 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -99,10 +99,10 @@ def index validate_arguments! quota_resp = api.request( :expects => [200, 404], - :method => :get, - :path => "/apps/#{app}/quota", + :method => :post, + :path => "/apps/#{app}/actions/get-quota", :headers => { - "Accept" => "application/vnd.heroku+json; version=3", + "Accept" => "application/vnd.heroku+json; version=3.app-quotas", "Content-Type" => "application/json" } ) diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index a84876040..aee0118d0 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -45,7 +45,7 @@ :status => 200 ) Excon.stub( - { :method => :get, :path => "/apps/example/quota" }, + { :method => :post, :path => "/apps/example/actions/get-quota" }, :status => 404 ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(10).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") @@ -85,7 +85,7 @@ :status => 200 ) Excon.stub( - { :method => :get, :path => "/apps/example/quota" }, + { :method => :post, :path => "/apps/example/actions/get-quota" }, :status => 404 ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return('2012/09/11 12:34:56 (~ 0s ago)') @@ -117,7 +117,7 @@ :status => 200 ) Excon.stub( - { :method => :get, :path => "/apps/example/quota" }, + { :method => :post, :path => "/apps/example/actions/get-quota" }, :status => 404 ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") @@ -150,7 +150,7 @@ :status => 200 ) Excon.stub( - { :method => :get, :path => "/apps/example/quota" }, + { :method => :post, :path => "/apps/example/actions/get-quota" }, :status => 404 ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).twice.and_return("2012/09/11 12:34:56 (~ 0s ago)") @@ -184,7 +184,7 @@ :status => 200 ) Excon.stub( - { :method => :get, :path => "/apps/example/quota" }, + { :method => :post, :path => "/apps/example/actions/get-quota" }, :status => 404 ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).exactly(4).times.and_return("2012/09/11 12:34:56 (~ 0s ago)") @@ -222,13 +222,13 @@ :status => 200 ) Excon.stub( - { :method => :get, :path => "/apps/example/quota" }, + { :method => :post, :path => "/apps/example/actions/get-quota" }, :body => { "allow_until" => allow_until.iso8601, "deny_until" => nil, }.to_json, - :status => 404 + :status => 200 ) expect_any_instance_of(Heroku::Command::Ps).to receive(:time_ago).once.times.and_return("2012/09/11 12:34:56 (~ 0s ago)") expect_any_instance_of(Heroku::Command::Ps).to receive(:time_remaining).and_return("20s") From e9f27b37c971c3fca8bd80faa6f1ca99e20a2060 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 1 May 2015 12:25:16 -0700 Subject: [PATCH 481/952] v3.33.0 --- CHANGELOG | 10 ++++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a1871dc7e..cbfd86b71 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +3.33.0 2015-05-01 +================= +Added shell flag to config:get +Renamed buildpack to buildpacks +Removed shell escaping from config:get --shell if it is not a tty +Fixed bug when api returned no certificate on certs:info +Added output for pg:backups commands +Merged the dyno-types plugin +Allow cedar-10 as stack in apps:create + 3.32.0 2015-04-21 ================= Added new fork implementation diff --git a/Gemfile.lock b/Gemfile.lock index 165d42401..6a5d0e68e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.32.0) + heroku (3.33.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 86a89df47..dceaa1a8d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.32.0" + VERSION = "3.33.0" end From c47be0dcb8b9bcff287ef85b93141e1ea60cae06 Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Tue, 21 Apr 2015 13:47:26 -0700 Subject: [PATCH 482/952] Pull in heroku-addon-attachments and update tests This pulls in the code from the heroku-addon-attachments plugin and updates the appropriate tests to work with the new code and output. --- lib/heroku/command/addons.rb | 512 +++++++++++-------- lib/heroku/command/pg.rb | 24 +- lib/heroku/helpers/addons/api.rb | 98 ++++ lib/heroku/helpers/addons/display.rb | 134 +++++ lib/heroku/helpers/addons/resolve.rb | 134 +++++ spec/heroku/command/addons_spec.rb | 702 ++++++++++++++++----------- spec/heroku/command/config_spec.rb | 12 + spec/heroku/command/pg_spec.rb | 30 +- spec/heroku/command_spec.rb | 31 +- spec/spec_helper.rb | 1 + spec/support/addons_helper.rb | 52 ++ 11 files changed, 1244 insertions(+), 486 deletions(-) create mode 100644 lib/heroku/helpers/addons/api.rb create mode 100644 lib/heroku/helpers/addons/display.rb create mode 100644 lib/heroku/helpers/addons/resolve.rb create mode 100644 spec/support/addons_helper.rb diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 865b82702..03a013620 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -1,5 +1,8 @@ require "heroku/command/base" require "heroku/helpers/heroku_postgresql" +require "heroku/helpers/addons/api" +require "heroku/helpers/addons/display" +require "heroku/helpers/addons/resolve" module Heroku::Command @@ -8,178 +11,372 @@ module Heroku::Command class Addons < Base include Heroku::Helpers::HerokuPostgresql + include Heroku::Helpers::Addons::API + include Heroku::Helpers::Addons::Display + include Heroku::Helpers::Addons::Resolve - # addons + # addons [{--all,--app APP_NAME,--resource ADDON_NAME}] # - # list installed addons + # list installed add-ons + # + # NOTE: --all is the default unless in an application repository directory, in + # which case --all is inferred. + # + # --all # list add-ons across all apps in account + # --app APP_NAME # list add-ons associated with a given app + # --resource ADDON_NAME # view details about add-on and all of its attachments + # + #Examples: + # + # $ heroku addons --all + # $ heroku addons --app acme-inc-website + # $ heroku addons --resource @acme-inc-database # def index validate_arguments! - - installed = api.get_addons(app).body - if installed.empty? - display("#{app} has no add-ons.") + requires_preauth + + # Filters are mutually exclusive + error("Can not use --all with --app") if options[:app] && options[:all] + error("Can not use --all with --resource") if options[:resource] && options[:all] + error("Can not use --app with --resource") if options[:resource] && options[:app] + + app = (self.app rescue nil) + if (resource = options[:resource]) + show_for_resource(resource) + elsif app && !options[:all] + show_for_app(app) else - available, pending = installed.partition { |a| a['configured'] } + show_all + end + end - unless available.empty? - styled_header("#{app} Configured Add-ons") - styled_array(available.map do |a| - [a['name'], a['attachment_name'] || ''] - end) - end + # addons:services + # + # list all available add-on services + def services + if current_command == "addons:list" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:services` instead.") + end - unless pending.empty? - styled_header("#{app} Add-ons to Configure") - styled_array(pending.map do |a| - [a['name'], app_addon_url(a['name'])] - end) - end + display_table(get_services, %w[name human_name state], %w[Slug Name State]) + display "\nSee plans with `heroku addons:plans SERVICE`" + end + + alias_command "addons:list", "addons:services" + + # addons:plans SERVICE + # + # list all available plans for an add-on service + def plans + service = args.shift + raise CommandFailed.new("Missing add-on service") if service.nil? + + service = get_service!(service) + display_header("#{service['human_name']} Plans") + + plans = get_plans(:service => service['id']) + + plans = plans.sort_by { |p| [(!p['default']).to_s, p['price']['cents']] }.map do |plan| + { + "default" => ('default' if plan['default']), + "name" => plan["name"], + "human_name" => plan["human_name"], + "price" => format_price(plan["price"]) + } end + + display_table(plans, %w[default name human_name price], [nil, 'Slug', 'Name', 'Price']) end - # addons:list + # addons:create {SERVICE,PLAN} + # + # create an add-on resource # - # list all available addons + # --name ADDON_NAME # (optional) name for the add-on resource + # --as ATTACHMENT_NAME # (optional) name for the initial add-on attachment + # --confirm APP_NAME # (optional) ovewrite existing config vars or existing add-on attachments # - # --region REGION # specify a region for addon availability + def create + if current_command == "addons:add" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:create` instead.") + end + + requires_preauth + + service_plan = args.shift + raise CommandFailed.new("Missing requested service or plan") if service_plan.nil? || %w{--fork --follow --rollback}.include?(service_plan) + + config = parse_options(args) + raise CommandFailed.new("Unexpected arguments: #{args.join(' ')}") unless args.empty? + + addon = request( + :body => json_encode({ + "attachment" => { "name" => options[:as] }, + "config" => config, + "name" => options[:name], + "confirm" => options[:confirm], + "plan" => { "name" => service_plan } + }), + :headers => { + # Temporary hack for getting provider messages while a cleaner + # endpoint is designed to communicate this data. + # + # WARNING: Do not depend on this having any effect permanently. + "X-Heroku-Legacy-Provider-Messages" => "true" + }, + :expects => 201, + :method => :post, + :path => "/apps/#{app}/addons" + ) + + action("Creating #{addon['name'].downcase}") {} + action("Adding #{addon['name'].downcase} to #{app}") {} + + if addon['config_vars'].any? + action("Setting #{addon['config_vars'].join(', ')} and restarting #{app}") do + @status = api.get_release(app, 'current').body['name'] + end + end + + display addon['provision_message'] unless addon['provision_message'].to_s.strip == "" + + display("Use `heroku addons:docs #{addon['addon_service']['name']}` to view documentation.") + end + + alias_command "addons:add", "addons:create" + + # addons:attach ADDON_NAME # - #Example: + # attach add-on resource to an app # - # $ heroku addons:list --region eu - # === available - # adept-scale:battleship, corvette... - # adminium:enterprise, petproject... + # --as ATTACHMENT_NAME # (optional) name for add-on attachment + # --confirm APP_NAME # overwrite existing add-on attachment with same name # - def list - addons = heroku.addons(options) - if addons.empty? - display "No addons available currently" - else - partitioned_addons = partition_addons(addons) - partitioned_addons.each do |key, addons| - partitioned_addons[key] = format_for_display(addons) + def attach + unless addon_name = args.shift + error("Usage: heroku addons:attach ADDON_NAME\nMust specify add-on resource to attach.") + end + addon = resolve_addon!(addon_name) + + requires_preauth + + attachment_name = options[:as] + + msg = attachment_name ? + "Attaching #{addon['name']} as #{attachment_name} to #{app}" : + "Attaching #{addon['name']} to #{app}" + + display("#{msg}... ", false) + + response = api.request( + :body => json_encode({ + "app" => {"name" => app}, + "addon" => {"name" => addon['name']}, + "confirm" => options[:confirm], + "name" => attachment_name + }), + :expects => [201, 422], + :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :method => :post, + :path => "/addon-attachments" + ) + + case response.status + when 201 + display("done") + action("Setting #{response.body["name"]} vars and restarting #{app}") do + @status = api.get_release(app, 'current').body['name'] end - display_object(partitioned_addons) + when 422 # add-on resource not found or cannot be attached + display("failed") + output_with_bang(response.body["message"]) + output_with_bang("List available resources with `heroku addons`.") + output_with_bang("Provision a new add-on resource with `heroku addons:create ADDON_PLAN`.") end end - # addons:add ADDON + # addons:detach ATTACHMENT_NAME # - # install an addon + # detach add-on resource from an app # - def add - configure_addon('Adding') do |addon, config| - heroku.install_addon(app, addon, config) + def detach + attachment_name = args.shift + raise CommandFailed.new("Missing add-on attachment name") if attachment_name.nil? + requires_preauth + + addon_attachment = resolve_attachment!(attachment_name) + + attachment_name = addon_attachment['name'] # in case a UUID was passed in + addon_name = addon_attachment['addon']['name'] + app = addon_attachment['app']['name'] + + action("Removing #{attachment_name} attachment to #{addon_name} from #{app}") do + api.request( + :expects => 200..300, + :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :method => :delete, + :path => "/addon-attachments/#{addon_attachment['id']}" + ).body + end + action("Unsetting #{attachment_name} vars and restarting #{app}") do + @status = api.get_release(app, 'current').body['name'] end end - # addons:upgrade ADDON + # addons:upgrade ADDON_NAME PLAN # - # upgrade an existing addon + # upgrade an existing add-on resource to PLAN # def upgrade - configure_addon('Upgrading to') do |addon, config| - heroku.upgrade_addon(app, addon, config) + addon_name, plan = args.shift, args.shift + + if addon_name && !plan # If invocated as `addons:Xgrade service:plan` + deprecate("No add-on name specified (see `heroku help #{current_command}`)") + + addon = nil + plan = addon_name + service = plan.split(':').first + + action("Finding add-on from service #{service} on app #{app}") do + # resolve with the service only, because the user has passed in the + # *intended* plan, not the current plan. + addon = resolve_addon!(service) + addon_name = addon['name'] + end + display "Found #{addon_name} (#{addon['plan']['name']}) on #{app}." + else + raise CommandFailed.new("Missing add-on name") if addon_name.nil? + addon_name = addon_name.sub(/^@/, '') + end + + raise CommandFailed.new("Missing add-on plan") if plan.nil? + + action("Changing #{addon_name} plan to #{plan}") do + api.request( + :body => json_encode({ + "plan" => { "name" => plan } + }), + :expects => 200..300, + :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :method => :patch, + :path => "/apps/#{app}/addons/#{addon_name}" + ) end end - # addons:downgrade ADDON + # addons:downgrade ADDON_NAME PLAN # - # downgrade an existing addon + # downgrade an existing add-on resource to PLAN # def downgrade - configure_addon('Downgrading to') do |addon, config| - heroku.upgrade_addon(app, addon, config) - end + upgrade end - # addons:remove ADDON1 [ADDON2 ...] + # addons:destroy ADDON_NAME [ADDON_NAME ...] + # + # destroy add-on resources # - # uninstall one or more addons + # -f, --force # allow destruction even if add-on is attached to other apps # - def remove - return unless confirm_command + def destroy + if current_command == "addons:remove" + deprecate("`heroku #{current_command}` has been deprecated. Please use `heroku addons:destroy` instead.") + end + + raise CommandFailed.new("Missing add-on name") if args.empty? + + requires_preauth + confirmed_apps = [] - args.each do |name| - messages = nil - if name.start_with? "HEROKU_POSTGRESQL_" - name = name.chomp("_URL").freeze + while addon_name = args.shift + addon = resolve_addon!(addon_name) + app = addon['app'] + + unless confirmed_apps.include?(app['name']) + return unless confirm_command(app['name']) + confirmed_apps << app['name'] + end + + addon_attachments = get_attachments(:resource => addon['id']) + + action("Destroying #{addon['name']} on #{app['name']}") do + api.request( + :body => json_encode({ + "force" => options[:force], + }), + :expects => 200..300, + :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :method => :delete, + :path => "/apps/#{app['id']}/addons/#{addon['id']}" + ) end - action("Removing #{name} on #{app}") do - messages = addon_run { heroku.uninstall_addon(app, name, :confirm => app) } + + if addon['config_vars'].any? # litmus test for whether the add-on's attachments have vars + # For each app that had an attachment, output a message indicating that + # the app has been restarted any any associated vars have been removed. + addon_attachments.group_by { |att| att['app']['name'] }.each do |app, attachments| + names = attachments.map { |att| att['name'] }.join(', ') + action("Removing vars for #{names} from #{app} and restarting") { + @status = api.get_release(app, 'current').body['name'] + } + end end - display(messages[:attachment]) if messages[:attachment] - display(messages[:message]) if messages[:message] end end - # addons:docs ADDON + alias_command "addons:remove", "addons:destroy" + + # addons:docs ADDON_NAME # - # open an addon's documentation in your browser + # open an add-on's documentation in your browser # def docs - unless addon = shift_argument + unless identifier = shift_argument error("Usage: heroku addons:docs ADDON\nMust specify ADDON to open docs for.") end validate_arguments! - addon_names = api.get_addons.body.map {|a| a['name']} - addon_types = addon_names.map {|name| name.split(':').first}.uniq - - name_matches = addon_names.select {|name| name =~ /^#{addon}/} - type_matches = addon_types.select {|name| name =~ /^#{addon}/} - - if name_matches.include?(addon) || type_matches.include?(addon) - type_matches = [addon] - end - - case type_matches.length - when 0 then - error([ - "`#{addon}` is not a heroku add-on.", - suggestion(addon, addon_names + addon_types), - "See `heroku addons:list` for all available addons." - ].compact.join("\n")) - when 1 - addon_type = type_matches.first - launchy("Opening #{addon_type} docs", addon_docs_url(addon_type)) + # If it looks like a plan, optimistically open docs, otherwise try to + # lookup a corresponding add-on and open the docs for its service. + if identifier.include?(':') + service = identifier.split(':')[0] + launchy("Opening #{service} docs", addon_docs_url(service)) else - error("Ambiguous addon name: #{addon}\nPerhaps you meant #{name_matches[0...-1].map {|match| "`#{match}`"}.join(', ')} or `#{name_matches.last}`.\n") + # searching by any number of things + matches = resolve_addon(identifier) + services = matches.map { |m| m['addon_service']['name'] }.uniq + + case services.count + when 0 + # Optimistically open docs for whatever they passed in + launchy("Opening #{identifier} docs", addon_docs_url(identifier)) + when 1 + service = services.first + launchy("Opening #{service} docs", addon_docs_url(service)) + else + error("Multiple add-ons match #{identifier.inspect}.\n" + + "Use the name of one of the add-on resources:\n\n" + + matches.map { |a| "- #{a['name']} (#{a['addon_service']['name']})" }.join("\n")) + end end end - # addons:open ADDON + # addons:open ADDON_NAME # - # open an addon's dashboard in your browser + # open an add-on's dashboard in your browser # def open - unless addon = shift_argument + unless addon_name = shift_argument error("Usage: heroku addons:open ADDON\nMust specify ADDON to open.") end validate_arguments! - app_addons = api.get_addons(app).body.map {|a| a['name']} - matches = app_addons.select {|a| a =~ /^#{addon}/}.sort + addon = resolve_addon!(addon_name) + return addon if addon.is_a?(String) - case matches.length - when 0 then - addon_names = api.get_addons.body.map {|a| a['name']} - if addon_names.any? {|name| name =~ /^#{addon}/} - error("Addon not installed: #{addon}") - else - error([ - "`#{addon}` is not a heroku add-on.", - suggestion(addon, addon_names + addon_names.map {|name| name.split(':').first}.uniq), - "See `heroku addons:list` for all available addons." - ].compact.join("\n")) - end - when 1 then - addon_to_open = matches.first - launchy("Opening #{addon_to_open} for #{app}", app_addon_url(addon_to_open)) - else - error("Ambiguous addon name: #{addon}\nPerhaps you meant #{matches[0...-1].map {|match| "`#{match}`"}.join(', ')} or `#{matches.last}`.\n") - end + service = addon['addon_service']['name'] + launchy("Opening #{service} (#{addon['name']}) for #{addon['app']['name']}", addon["web_url"]) end private @@ -188,103 +385,6 @@ def addon_docs_url(addon) "https://devcenter.#{heroku.host}/articles/#{addon.split(':').first}" end - def app_addon_url(addon) - "https://addons-sso.heroku.com/apps/#{app}/addons/#{addon}" - end - - def partition_addons(addons) - addons.group_by{ |a| (a["state"] == "public" ? "available" : a["state"]) } - end - - def format_for_display(addons) - grouped = addons.inject({}) do |base, addon| - group, short = addon['name'].split(':') - base[group] ||= [] - base[group] << addon.merge('short' => short) - base - end - grouped.keys.sort.map do |name| - addons = grouped[name] - row = name.dup - if addons.any? { |a| a['short'] } - row << ':' - size = row.size - stop = false - row << addons.map { |a| a['short'] }.compact.sort.map do |short| - size += short.size - if size < 31 - short - else - stop = true - nil - end - end.compact.join(', ') - row << '...' if stop - end - row - end - end - - def addon_run - response = yield - - if response - price = "(#{ response['price'] })" if response['price'] - - if response['message'] =~ /(Attached as [A-Z0-9_]+)\n(.*)/m - attachment = $1 - message = $2 - else - attachment = nil - message = response['message'] - end - - begin - release = api.get_release(app, 'current').body - release = release['name'] - rescue Heroku::API::Errors::Error - release = nil - end - end - - status [ release, price ].compact.join(' ') - { :attachment => attachment, :message => message } - end - - def configure_addon(label, &install_or_upgrade) - addon = args.shift - raise CommandFailed.new("Missing add-on name") if addon.nil? || %w{--fork --follow --rollback}.include?(addon) - - config = parse_options(args) - addon_name, plan = addon.split(':') - - # For Heroku Postgres, if no plan is specified with fork/follow/rollback, - # default to the plan of the current postgresql plan - if addon_name =~ /heroku-postgresql/ then - hpg_flag = %w{rollback fork follow}.select {|flag| config.keys.include? flag}.first - if plan.nil? && config[hpg_flag] =~ /^postgres:\/\// then - raise CommandFailed.new("Cross application database Forking/Following requires you specify a plan type") - elsif (hpg_flag && plan.nil?) then - resolver = Resolver.new(app, api) - addon = addon + ':' + resolver.resolve(config[hpg_flag]).plan - end - end - - config.merge!(:confirm => app) if app == options[:confirm] - raise CommandFailed.new("Unexpected arguments: #{args.join(' ')}") unless args.empty? - - hpg_translate_db_opts_to_urls(addon, config) - - messages = nil - action("#{label} #{addon} on #{app}") do - messages = addon_run { install_or_upgrade.call(addon, config) } - end - display(messages[:attachment]) unless messages[:attachment].to_s.strip == "" - display(messages[:message]) unless messages[:message].to_s.strip == "" - - display("Use `heroku addons:docs #{addon_name}` to view documentation.") - end - #this will clean up when we officially deprecate def parse_options(args) config = {} diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 1b1d4654f..4fce2e24c 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -4,7 +4,8 @@ require "heroku/command/base" require "heroku/helpers/heroku_postgresql" require "heroku/helpers/pg_dump_restore" - +require "heroku/helpers/addons/resolve" +require "heroku/helpers/addons/api" require "heroku/helpers/pg_diagnose" # manage heroku-postgresql databases @@ -19,6 +20,8 @@ def set_commands(shorthand) include Heroku::Helpers::HerokuPostgresql include Heroku::Helpers::PgDiagnose + include Heroku::Helpers::Addons::Resolve + include Heroku::Helpers::Addons::API # pg # @@ -82,10 +85,21 @@ def promote end validate_arguments! - attachment = generate_resolver.resolve(db) - - action "Promoting #{attachment.display_name} to DATABASE_URL" do - hpg_promote(attachment.url) + addon = resolve_addon!(db) + + attachment_name = 'DATABASE' + action "Promoting #{addon['name']} to #{attachment_name}_URL on #{app}" do + request( + :body => json_encode({ + "app" => {"name" => app}, + "addon" => {"name" => addon['name']}, + "confirm" => app, + "name" => attachment_name + }), + :expects => 201, + :method => :post, + :path => "/addon-attachments" + ) end end diff --git a/lib/heroku/helpers/addons/api.rb b/lib/heroku/helpers/addons/api.rb new file mode 100644 index 000000000..32a1a1193 --- /dev/null +++ b/lib/heroku/helpers/addons/api.rb @@ -0,0 +1,98 @@ +module Heroku::Helpers + module Addons + module API + VERSION="3.switzerland".freeze + + def request(options = {}) + defaults = { + :expects => 200, + :headers => {}, + :method => :get + } + options = defaults.merge(options) + options[:headers]["Accept"] ||= "application/vnd.heroku+json; version=#{VERSION}" + api.request(options).body + end + + def request_list(options = {}) + options = options.dup + options[:expects] = [200, 206, *options[:expects]].uniq + + request(options) + end + + def get_attachments(options = {}) + request_list(:path => attachments_path(options)) + end + + def get_attachment!(identifier, options = {}) + request(:path => "#{attachments_path(options)}/#{identifier}") + end + + def get_attachment(identifier, options = {}) + get_attachment!(identifier, options) + rescue Heroku::API::Errors::NotFound + end + + def get_addons(options = {}) + request_list( + :headers => { 'Accept-Expansion' => 'plan' }, + :path => addons_path(options) + ) + end + + def get_addon!(identifier, options = {}) + request( + :headers => { 'Accept-Expansion' => 'plan' }, + :path => "#{addons_path(options)}/#{identifier}" + ) + end + + def get_addon(identifier, options = {}) + get_addon!(identifier, options) + rescue Heroku::API::Errors::NotFound + end + + def get_service!(service) + request(:path => "/addon-services/#{service}") + end + + def get_service(service) + get_service! + rescue Heroku::API::Errors::NotFound + end + + def get_services + request_list(:path => "/addon-services") + end + + def get_plans(options = {}) + path = options[:service] ? + "/addon-services/#{options[:service]}/plans" : + "/plans" + + request_list(:path => path) + end + + private + + def addons_path(options) + if app = options[:app] + "/apps/#{app}/addons" + else + "/addons" + end + end + + def attachments_path(options) + if resource = options[:resource] + "/addons/#{resource}/addon-attachments" + elsif app = options[:app] + "/apps/#{app}/addon-attachments" + else + "/addon-attachments" + end + end + end + end +end diff --git a/lib/heroku/helpers/addons/display.rb b/lib/heroku/helpers/addons/display.rb new file mode 100644 index 000000000..bdb6c1144 --- /dev/null +++ b/lib/heroku/helpers/addons/display.rb @@ -0,0 +1,134 @@ +require "heroku/helpers/addons/api" + +module Heroku::Helpers + module Addons + module Display + include Heroku::Helpers::Addons::API + + # Shows details about and attachments for a specified resource. For example: + # + # $ heroku addons --resource practicing-nobly-1495 + # === Resource Info + # Name: practicing-nobly-1495 + # Plan: heroku-postgresql:premium-yanari + # Billing App: addons-reports + # Price: $200.00/month + # + # === Attachments + # App Name + # -------------- ------------------------ + # addons ADDONS_REPORTS + # addons-reports DATABASE + # addons-reports HEROKU_POSTGRESQL_SILVER + def show_for_resource(identifier) + styled_header("Resource Info") + + resource = resolve_addon!(identifier) + + styled_hash({ + 'Name' => resource['name'], + 'Plan' => resource['plan']['name'], + 'Billing App' => resource['app']['name'], + 'Price' => format_price(resource['plan']['price']) + }, ['Name', 'Plan', 'Billing App', 'Price']) + + display("") # separate sections + + styled_header("Attachments") + display_attachments(get_attachments(:resource => identifier), ['App', 'Name']) + end + + # Shows all add-ons owned by and attachments attached to the provided app. For example: + # + # === Add-on Resources for bjeanes + # Plan Name Price + # ----------------------- ---------------------- ----- + # heroku-postgresql:dev budding-busily-2230 free + # memcachier-staging:test sighing-ably-6278 free + # memcachier-staging:test rolling-carefully-8506 free + # newrelic:wayne unwinding-kindly-4330 free + # pgbackups:plus pgbackups-8071074 free + # + # === Add-on Attachments for bjeanes + # Name Add-on Billing App + # ------------------------ ---------------------- ----------- + # DATABASE budding-busily-2230 bjeanes + # HEROKU_POSTGRESQL_VIOLET budding-busily-2230 bjeanes + # MEMCACHE sighing-ably-6278 bjeanes + # MEMCACHIER_STAGING rolling-carefully-8506 bjeanes + # NEWRELIC unwinding-kindly-4330 bjeanes + # PGBACKUPS pgbackups-8071074 bjeanes + def show_for_app(app) + styled_header("Resources for #{app}") + + addons = get_addons(:app => app). + # the /apps/:id/addons endpoint can return more than just those owned + # by the app, so filter: + select { |addon| addon['app']['name'] == app } + + display_addons(addons, %w[Plan Name Price]) + + display('') # separate sections + + styled_header("Attachments for #{app}") + display_attachments(get_attachments(:app => app), ['Name', 'Add-on', 'Billing App']) + end + + # Shows a table of all add-ons on the account. For example: + # + # === Add-on Resources + # Plan Name Billing App Price + # ----------------------- --------------------------- -------------- ------------ + # bugsnag:sagittaron bugsnag-9174150 addons $9.00/month + # deployhooks:hipchat deployhooks-hipchat-9852225 addons-staging free + # heroku-postgresql:crane advising-fairly-3183 ion-bo $50.00/month + # newrelic:wayne unwinding-kindly-4330 bjeanes free + def show_all + styled_header('Resources') + display_addons(get_addons, ['Plan', 'Name', 'Billed to', 'Price']) + end + + def display_attachments(attachments, fields) + if attachments.empty? + display('There are no attachments.') + else + table = attachments.map do |attachment| + { + 'Name' => attachment['name'], + 'Add-on' => attachment['addon']['name'], + 'Billing App' => attachment['addon']['app']['name'], + 'App' => attachment['app']['name'] + } + end.sort_by { |addon| fields.map { |f| addon[f] } } + + display_table(table, fields, fields) + end + end + + def display_addons(addons, fields) + if addons.empty? + display('There are no add-ons.') + else + table = addons.map do |addon| + { + 'Plan' => addon['plan']['name'], + 'Name' => addon['name'], + 'Billed to' => addon['app']['name'], + 'Price' => format_price(addon['plan']['price']) + } + end.sort_by { |addon| fields.map { |f| addon[f] } } + + display_table(table, fields, fields) + end + end + + def format_price(price) + if price['cents'] == 0 + 'free' + else + '$%.2f/%s' % [(price['cents'] / 100.0), price['unit']] + end + end + end + end +end diff --git a/lib/heroku/helpers/addons/resolve.rb b/lib/heroku/helpers/addons/resolve.rb new file mode 100644 index 000000000..3d3bead6a --- /dev/null +++ b/lib/heroku/helpers/addons/resolve.rb @@ -0,0 +1,134 @@ +require "heroku/helpers/addons/api" + +module Heroku::Helpers + module Addons + module Resolve + include Heroku::Helpers::Addons::API + + UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + ATTACHMENT = /^(?:([a-z][a-z0-9-]+)::)?([A-Z][A-Z0-9_]+)$/ + RESOURCE = /^@?([a-z][a-z0-9-]+)$/ + SERVICE_PLAN = /^(?:([a-z0-9_-]+):)?([a-z0-9_-]+)$/ # service / service:plan + + class AddonDoesNotExistError < Heroku::API::Errors::Error + end + + # Finds attachments that match provided identifier. + # + # Always returns an Array of 0 or more results. + def resolve_attachment(identifier) + case identifier + when UUID + [get_attachment(identifier)].compact + when ATTACHMENT + app = $1 || self.app # "app::..." or current app + name = $2 + + attachment = begin + get_attachment(name, :app => app) + rescue Heroku::API::Errors::NotFound + end + + return [attachment] if attachment + + get_attachments(:app => app).select { |att| att["name"][name] } + else + [] + end + end + + # Finds a single attachment unambiguously given an identifier. + # + # Returns an attachment hash or exits with an error. + def resolve_attachment!(identifier) + results = resolve_attachment(identifier) + + case results.count + when 1 + results[0] + when 0 + error("Can not find attachment with #{identifier.inspect}") + else + app = results.first['app']['name'] + error("Multiple attachments on #{app} match #{identifier.inspect}.\n" + + "Did you mean one of:\n\n" + + results.map { |att| "- #{att['name']}" }.join("\n")) + end + end + + # Finds add-ons that match provided identifier. + # + # Supports: + # * add-on resource UUID + # * add-on resource name (@my-db / my-db) + # * attachment name (other-app::ATTACHMENT / ATTACHMENT on current app) + # * service name + # * service:plan name + # + # Returns an array in every case except for when using a service name for an + # non-existent add-on. In that case, the error message is returned. + # + def resolve_addon(identifier) + case identifier + when UUID + return [get_addon(identifier)].compact + when ATTACHMENT + # identifier -> Array[Attachment] -> uniq Array[Addon] + matches = resolve_attachment(identifier) + matches. + map { |att| att['addon']['id'] }. + uniq. + map { |addon_id| get_addon(addon_id) } + else # try both resource and service identifiers, because they look similar + if identifier =~ RESOURCE + name = $1 + + addon = begin + get_addon(name) + rescue Heroku::API::Errors::Forbidden + # treat permission error as no match because there might exist a + # resource on someone else's app that has a name which + # corresponds to a service name that we wish to check below (e.g. + # "memcachier") + end + + return [addon] if addon + end + + if identifier =~ SERVICE_PLAN + service_name, plan_name = *[$1, $2].compact + full_plan_name = [service_name, plan_name].join(':') if plan_name + + addons = get_addons(:app => app).select do |addon| + addon['addon_service']['name'] == service_name && # match service + [nil, addon['plan']['name']].include?(full_plan_name) && # match plan, IFF specified + addon['app']['name'] == app # /apps/:id/addons returns un-owned add-ons + end + + return addons + end + + [] + end + end + + # Finds a single add-on unambiguously given an identifier. + # + # Returns an add-on hash or exits with an error. + def resolve_addon!(identifier) + results = resolve_addon(identifier) + + case results.count + when 1 + results[0] + when 0 + error("Can not find add-on with #{identifier.inspect}") + else + error("Multiple add-ons match #{identifier.inspect}.\n" + + "Use the name of add-on resource:\n\n" + + results.map { |a| "- #{a['name']} (#{a['plan']['name']})" }.join("\n")) + end + end + end + end +end diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 83a2f6be7..fdfb52e0b 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -3,13 +3,15 @@ module Heroku::Command describe Addons do + include Support::Addons + let(:addon) { build_addon(name: "my_addon", app: { name: "example" }) } + before do @addons = prepare_command(Addons) stub_core.release("example", "current").returns( "name" => "v99" ) end - describe "index" do - + describe "#index" do before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") @@ -20,170 +22,254 @@ module Heroku::Command end it "should display no addons when none are configured" do + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: "[]", status: 200 } + end + + Excon.stub(method: :get, path: %r(/apps/example/addon-attachments)) do + { body: "[]", status: 200 } + end + stderr, stdout = execute("addons") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -example has no add-ons. +=== Resources for example +There are no add-ons. + +=== Attachments for example +There are no attachments. STDOUT + + Excon.stubs.shift(2) end it "should list addons and attachments" do - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/addons$} - }, - { - :body => MultiJson.dump([ - { 'configured' => false, 'name' => 'deployhooks:email' }, - { 'attachment_name' => 'HEROKU_POSTGRESQL_RED', 'configured' => true, 'name' => 'heroku-postgresql:ronin' }, - { 'configured' => true, 'name' => 'deployhooks:http' } - ]), - :status => 200, - } - ) + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + hooks = build_addon( + name: "swimming-nicely-42", + plan: { name: "deployhooks:http", price: { cents: 0, unit: "month" }}, + app: { name: "example" }) + + hpg = build_addon( + name: "jumping-slowly-76", + plan: { name: "heroku-postgresql:ronin", price: { cents: 20000, unit: "month" }}, + app: { name: "example" }) + + { body: MultiJson.encode([hooks, hpg]), status: 200 } + end + + Excon.stub(method: :get, path: %(/apps/example/addon-attachments)) do + hpg = build_attachment( + name: "HEROKU_POSTGRESQL_CYAN", + addon: { name: "heroku-postgresql-12345", app: { name: "example" }}, + app: { name: "example" }) + + { body: MultiJson.encode([hpg]), status: 200 } + end + stderr, stdout = execute("addons") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -=== example Configured Add-ons -deployhooks:http -heroku-postgresql:ronin HEROKU_POSTGRESQL_RED - -=== example Add-ons to Configure -deployhooks:email https://addons-sso.heroku.com/apps/example/addons/deployhooks:email - +=== Resources for example +Plan Name Price +----------------------- ------------------ ------------- +deployhooks:http swimming-nicely-42 free +heroku-postgresql:ronin jumping-slowly-76 $200.00/month + +=== Attachments for example +Name Add-on Billing App +---------------------- ----------------------- ----------- +HEROKU_POSTGRESQL_CYAN heroku-postgresql-12345 example STDOUT - Excon.stubs.shift + Excon.stubs.shift(2) end end describe "list" do + before do + Excon.stub(method: :get, path: %r(/addon-services)) do + services = [ + { "name" => "cloudcounter:basic", "state" => "alpha" }, + { "name" => "cloudcounter:pro", "state" => "public" }, + { "name" => "cloudcounter:gold", "state" => "public" }, + { "name" => "cloudcounter:old", "state" => "disabled" }, + { "name" => "cloudcounter:platinum", "state" => "beta" } + ] + + { body: MultiJson.encode(services), status: 200 } + end + end - it "sends region option to the server" do - stub_request(:get, %r{/addons\?region=eu$}). + after do + Excon.stubs.shift + end + + # TODO: plugin code doesn't support this. Do we need it? + xit "sends region option to the server" do + stub_request(:get, %r{/addon-services\?region=eu$}). to_return(:body => MultiJson.dump([])) execute("addons:list --region=eu") end - it "lists available addons" do - stub_core.addons.returns([ - { "name" => "cloudcounter:basic", "state" => "alpha" }, - { "name" => "cloudcounter:pro", "state" => "public" }, - { "name" => "cloudcounter:gold", "state" => "public" }, - { "name" => "cloudcounter:old", "state" => "disabled" }, - { "name" => "cloudcounter:platinum", "state" => "beta" } - ]) - stderr, stdout = execute("addons:list") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== alpha -cloudcounter:basic - -=== available -cloudcounter:gold, pro - -=== beta -cloudcounter:platinum - -=== disabled -cloudcounter:old + describe "when using the deprecated `addons:list` command" do + it "displays a deprecation warning" do + stderr, stdout = execute("addons:list") + expect(stderr).to eq("") + expect(stdout).to include "WARNING: `heroku addons:list` has been deprecated. Please use `heroku addons:services` instead." + end + end -STDOUT + describe "when using correct `addons:services` command" do + it "displays all services" do + stderr, stdout = execute("addons:services") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Slug Name State +--------------------- ---- -------- +cloudcounter:basic alpha +cloudcounter:pro public +cloudcounter:gold public +cloudcounter:old disabled +cloudcounter:platinum beta + +See plans with `heroku addons:plans SERVICE` + STDOUT + end end end describe 'v1-style command line params' do + before do + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + { body: MultiJson.encode(addon), status: 201 } + end + end + + after do + Excon.stubs.shift + end + it "understands foo=baz" do allow(@addons).to receive(:args).and_return(%w(my_addon foo=baz)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) - @addons.add + + allow(@addons.api).to receive(:request) { |params| + expect(params[:body]).to include '"foo":"baz"' + }.and_return(double(body: stringify(addon))) + + @addons.create end - it "gives a deprecation notice with an example" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:config => {:foo => 'bar', :extra => "XXX"}}). - to_return(:body => MultiJson.dump({ 'price' => 'free' })) - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/releases/current} - }, - { - :body => MultiJson.dump({ 'name' => 'v99' }), - :status => 200, - } - ) - stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Warning: non-unix style params have been deprecated, use --extra=XXX instead -Adding my_addon on example... done, v99 (free) -Use `heroku addons:docs my_addon` to view documentation. -STDOUT - Excon.stubs.shift + describe "addons:add" do + before do + Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do + { body: MultiJson.dump({ 'name' => 'v99' }), status: 200 } + end + + Excon.stub(method: :post, path: %r{apps/example/addons/my_addon$}) do + { body: MultiJson.encode(price: "free"), status: 200 } + end + end + + after do + Excon.stubs.shift(2) + end + + it "shows a deprecation warning about addon:add vs addons:create" do + stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") + expect(stderr).to eq("") + expect(stdout).to include "WARNING: `heroku addons:add` has been deprecated. Please use `heroku addons:create` instead." + end + + it "shows a deprecation warning about non-unix params" do + stderr, stdout = execute("addons:add my_addon --foo=bar extra=XXX") + expect(stderr).to eq("") + expect(stdout).to include "Warning: non-unix style params have been deprecated, use --extra=XXX instead" + end end end describe 'unix-style command line params' do it "understands --foo=baz" do allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) - @addons.add + + allow(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":"baz"' + }.and_return(stringify(addon)) + + @addons.create end it "understands --foo baz" do allow(@addons).to receive(:args).and_return(%w(my_addon --foo baz)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz' }) - @addons.add + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":"baz"' + }.and_return(stringify(addon)) + + @addons.create end it "treats lone switches as true" do allow(@addons).to receive(:args).and_return(%w(my_addon --foo)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) - @addons.add + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":true' + }.and_return(stringify(addon)) + + @addons.create end it "converts 'true' to boolean" do allow(@addons).to receive(:args).and_return(%w(my_addon --foo=true)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => true }) - @addons.add + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":true' + }.and_return(stringify(addon)) + + @addons.create end it "works with many config vars" do allow(@addons).to receive(:args).and_return(%w(my_addon --foo baz --bar yes --baz=foo --bab --bob=true)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => true, 'bob' => true }) - @addons.add - end - it "sends the variables to the server" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:config => { 'foo' => 'baz', 'bar' => 'yes', 'baz' => 'foo', 'bab' => 'true', 'bob' => 'true' }}) - stderr, stdout = execute("addons:add my_addon --foo baz --bar yes --baz=foo --bab --bob=true") - expect(stderr).to eq("") + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include({ foo: 'baz', bar: 'yes', baz: 'foo', bab: true, bob: true }.to_json) + }.and_return(stringify(addon)) + + @addons.create end it "raises an error for spurious arguments" do allow(@addons).to receive(:args).and_return(%w(my_addon spurious)) - expect { @addons.add }.to raise_error(CommandFailed) + expect { @addons.create }.to raise_error(CommandFailed) end end describe "mixed options" do it "understands foo=bar and --baz=bar on the same line" do allow(@addons).to receive(:args).and_return(%w(my_addon foo=baz --baz=bar bob=true --bar)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', { 'foo' => 'baz', 'baz' => 'bar', 'bar' => true, 'bob' => true }) - @addons.add + + expect(@addons).to receive(:request) { |args| + expect(args[:body]).to include '"foo":"baz"' + expect(args[:body]).to include '"baz":"bar"' + expect(args[:body]).to include '"bar":true' + expect(args[:body]).to include '"bob":true' + }.and_return(stringify(addon)) + + @addons.create end it "sends the variables to the server" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:config => { 'foo' => 'baz', 'baz' => 'bar', 'bar' => 'true', 'bob' => 'true' }}) + Excon.stub(method: :post, path: %r{/apps/example/addons$}) do + { body: MultiJson.encode(addon), status: 201 } + end + stderr, stdout = execute("addons:add my_addon foo=baz --baz=bar bob=true --bar") expect(stderr).to eq("") expect(stdout).to include("Warning: non-unix style params have been deprecated, use --foo=baz --bob=true instead") + + Excon.stubs.shift end end @@ -191,26 +277,12 @@ module Heroku::Command it "should only resolve for heroku-postgresql addon" do %w{fork follow rollback}.each do |switch| allow(@addons).to receive(:args).and_return("addon --#{switch} HEROKU_POSTGRESQL_RED".split) - expect(@addons.heroku).to receive(:install_addon). - with('example', 'addon', {switch => 'HEROKU_POSTGRESQL_RED'}) - @addons.add - end - end - it "should translate --fork, --follow, and --rollback" do - %w{fork follow rollback}.each do |switch| - allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver).to receive(:app_config_vars).and_return({}) - allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver).to receive(:app_attachments).and_return([Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, - 'name' => 'HEROKU_POSTGRESQL_RED', - 'config_var' => 'HEROKU_POSTGRESQL_RED_URL', - 'resource' => {'name' => 'loudly-yelling-1232', - 'value' => 'postgres://red_url', - 'type' => 'heroku-postgresql:ronin' }}) - ]) - allow(@addons).to receive(:args).and_return("heroku-postgresql --#{switch} HEROKU_POSTGRESQL_RED".split) - expect(@addons.heroku).to receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://red_url'}) - @addons.add + allow(@addons).to receive(:request) { |args| + expect(args[:body]).to include %("#{switch}":"HEROKU_POSTGRESQL_RED") + }.and_return(stringify(addon)) + + @addons.create end end @@ -219,17 +291,22 @@ module Heroku::Command allow(@addons).to receive(:app_config_vars).and_return({}) allow(@addons).to receive(:app_attachments).and_return([]) allow(@addons).to receive(:args).and_return("heroku-postgresql:ronin --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) - expect(@addons.heroku).to receive(:install_addon).with('example', 'heroku-postgresql:ronin', {switch => 'postgres://foo:yeah@awesome.com:234/bestdb'}) - @addons.add + + allow(@addons).to receive(:request) { |args| + expect(args[:body]).to include %("#{switch}":"postgres://foo:yeah@awesome.com:234/bestdb") + }.and_return(stringify(addon)) + + @addons.create end end - it "should fail if fork / follow across applications and no plan is specified" do + # TODO: ? + xit "should fail if fork / follow across applications and no plan is specified" do %w{fork follow}.each do |switch| allow(@addons).to receive(:app_config_vars).and_return({}) allow(@addons).to receive(:app_attachments).and_return([]) allow(@addons).to receive(:args).and_return("heroku-postgresql --#{switch} postgres://foo:yeah@awesome.com:234/bestdb".split) - expect { @addons.add }.to raise_error(CommandFailed) + expect { @addons.create }.to raise_error(CommandFailed) end end end @@ -249,6 +326,7 @@ module Heroku::Command } ) end + after do Excon.stubs.shift end @@ -256,54 +334,104 @@ module Heroku::Command it "requires an addon name" do allow(@addons).to receive(:args).and_return([]) - expect { @addons.add }.to raise_error(CommandFailed) + expect { @addons.create }.to raise_error(CommandFailed) end it "adds an addon" do allow(@addons).to receive(:args).and_return(%w(my_addon)) - expect(@addons.heroku).to receive(:install_addon).with('example', 'my_addon', {}) - @addons.add + + allow(@addons).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/example/addons" + expect(args[:body]).to include '"name":"my_addon"' + }.and_return(stringify(addon)) + + @addons.create end it "adds an addon with a price" do - stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "free" }) - stderr, stdout = execute("addons:add my_addon") + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" }) + + { body: MultiJson.encode(addon), status: 201 } + end + + stderr, stdout = execute("addons:create my_addon") expect(stderr).to eq("") - expect(stdout).to match(/\(free\)/) + expect(stdout).to match /Creating my_addon... done/ + + Excon.stubs.shift end it "adds an addon with a price and message" do - stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "foo" }) - stderr, stdout = execute("addons:add my_addon") + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" } + ).merge(provision_message: "OMG A MESSAGE") + + { body: MultiJson.encode(addon), status: 201 } + end + + stderr, stdout = execute("addons:create my_addon") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Adding my_addon on example... done, v99 (free) -foo +Creating my_addon... done +Adding my_addon to example... done +OMG A MESSAGE Use `heroku addons:docs my_addon` to view documentation. OUTPUT + + Excon.stubs.shift end it "excludes addon plan from docs message" do - stub_core.install_addon("example", "my_addon:test", {}).returns({ "price" => "free", "message" => "foo" }) - stderr, stdout = execute("addons:add my_addon:test") + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" }) + + { body: MultiJson.encode(addon), status: 201 } + end + + stderr, stdout = execute("addons:create my_addon:test") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Adding my_addon:test on example... done, v99 (free) -foo +Creating my_addon... done +Adding my_addon to example... done Use `heroku addons:docs my_addon` to view documentation. OUTPUT + + Excon.stubs.shift end it "adds an addon with a price and multiline message" do + Excon.stub(method: :post, path: %r(/apps/example/addons)) do + addon = build_addon( + name: "my_addon", + addon_service: { name: "my_addon" }, + app: { name: "example" } + ).merge(provision_message: "foo\nbar") + + { body: MultiJson.encode(addon), status: 201 } + end + stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "$200/mo", "message" => "foo\nbar" }) - stderr, stdout = execute("addons:add my_addon") + stderr, stdout = execute("addons:create my_addon") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Adding my_addon on example... done, v99 ($200/mo) +Creating my_addon... done +Adding my_addon to example... done foo bar Use `heroku addons:docs my_addon` to view documentation. OUTPUT + + Excon.stubs.shift end it "displays an error with unexpected options" do @@ -313,6 +441,12 @@ module Heroku::Command end describe 'upgrading' do + let(:addon) do + build_addon(name: "my_addon", + app: { name: "example" }, + plan: { name: "my_addon" }) + end + before do allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( @@ -327,6 +461,7 @@ module Heroku::Command } ) end + after do Excon.stubs.shift end @@ -337,54 +472,69 @@ module Heroku::Command end it "upgrades an addon" do + allow(@addons).to receive(:resolve_addon!).and_return(stringify(addon)) allow(@addons).to receive(:args).and_return(%w(my_addon)) - expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', {}) + + expect(@addons.api).to receive(:request) { |args| + expect(args[:method]).to eq :patch + expect(args[:path]).to eq "/apps/example/addons/my_addon" + } + @addons.upgrade end - it "upgrade an addon with config vars" do + # TODO: need this? + xit "upgrade an addon with config vars" do + allow(@addons).to receive(:resolve_addon!).and_return(stringify(addon)) allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) @addons.upgrade end - it "adds an addon with a price" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free" }) - stderr, stdout = execute("addons:upgrade my_addon") - expect(stderr).to eq("") - expect(stdout).to eq <<-OUTPUT -Upgrading to my_addon on example... done, v99 (free) -Use `heroku addons:docs my_addon` to view documentation. -OUTPUT - end + it "upgrades an addon with a price" do + my_addon = build_addon( + name: "my_addon", + plan: { name: "my_plan" }, + addon_service: { name: "my_service" }, + app: { name: "example" }) - it "adds an addon with a price and message" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "Don't Panic" }) - stderr, stdout = execute("addons:upgrade my_addon") + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :patch, path: %r(/apps/example/addons/my_addon)) do + { body: MultiJson.encode(my_addon), status: 200 } + end + + stderr, stdout = execute("addons:upgrade my_service") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Upgrading to my_addon on example... done, v99 (free) -Don't Panic -Use `heroku addons:docs my_addon` to view documentation. +WARNING: No add-on name specified (see `heroku help addons:upgrade`) +Finding add-on from service my_service on app example... done +Found my_addon (my_plan) on example. +Changing my_addon plan to my_service... done OUTPUT + + Excon.stubs.shift(2) end end describe 'downgrading' do + let(:addon) do + build_addon( + name: "my_addon", + addon_service: { name: "my_service" }, + plan: { name: "my_plan" }, + app: { name: "example" }) + end + before do - allow(@addons).to receive(:args).and_return(%w(my_addon)) Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/releases/current} - }, - { - :body => MultiJson.dump({ 'name' => 'v99' }), - :status => 200, - } + { :expects => 200, :method => :get, :path => %r{^/apps/example/releases/current} }, + { :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) end + after do Excon.stubs.shift end @@ -395,58 +545,98 @@ module Heroku::Command end it "downgrades an addon" do - allow(@addons).to receive(:args).and_return(%w(my_addon)) - expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', {}) + allow(@addons).to receive(:args).and_return(%w(my_service low_plan)) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:method]).to eq :patch + expect(args[:path]).to eq "/apps/example/addons/my_service" + }.and_return(stringify(addon)) + @addons.downgrade end it "downgrade an addon with config vars" do - allow(@addons).to receive(:args).and_return(%w(my_addon --foo=baz)) - expect(@addons.heroku).to receive(:upgrade_addon).with('example', 'my_addon', { 'foo' => 'baz' }) + allow(@addons).to receive(:args).and_return(%w(my_service --foo=baz)) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:method]).to eq :patch + expect(args[:path]).to eq "/apps/example/addons/my_service" + }.and_return(stringify(addon)) + @addons.downgrade end - it "downgrades an addon with a price" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free" }) - stderr, stdout = execute("addons:downgrade my_addon") - expect(stderr).to eq("") - expect(stdout).to eq <<-OUTPUT -Downgrading to my_addon on example... done, v99 (free) -Use `heroku addons:docs my_addon` to view documentation. -OUTPUT - end + describe "console output" do + before do + my_addon = build_addon( + name: "my_addon", + plan: { name: "my_plan" }, + addon_service: { name: "my_service" }, + app: { name: "example" }) - it "downgrades an addon with a price and message" do - stub_core.upgrade_addon("example", "my_addon", {}).returns({ "price" => "free", "message" => "Don't Panic" }) - stderr, stdout = execute("addons:downgrade my_addon") - expect(stderr).to eq("") - expect(stdout).to eq <<-OUTPUT -Downgrading to my_addon on example... done, v99 (free) -Don't Panic -Use `heroku addons:docs my_addon` to view documentation. + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :patch, path: %r(/apps/example/addons/my_service)) do + { body: MultiJson.encode(my_addon), status: 200 } + end + end + + after do + Excon.stubs.shift(2) + end + + it "downgrades an addon with a price" do + stderr, stdout = execute("addons:downgrade my_service low_plan") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +Changing my_service plan to low_plan... done OUTPUT + end end end - it "does not remove addons with no confirm" do + it "does not destroy addons with no confirm" do allow(@addons).to receive(:args).and_return(%w( addon1 )) + allow(@addons).to receive(:resolve_addon!).and_return({"app" => { "name" => "example" }}) expect(@addons).to receive(:confirm_command).once.and_return(false) - expect(@addons.heroku).not_to receive(:uninstall_addon) - @addons.remove + expect(@addons.api).not_to receive(:request).with(hash_including(method: :delete)) + @addons.destroy end - it "removes addons after prompting for confirmation" do + it "destroys addons after prompting for confirmation" do allow(@addons).to receive(:args).and_return(%w( addon1 )) expect(@addons).to receive(:confirm_command).once.and_return(true) - expect(@addons.heroku).to receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") - @addons.remove + allow(@addons).to receive(:get_attachments).and_return([]) + allow(@addons).to receive(:resolve_addon!).and_return({ + "id" => "abc123", + "config_vars" => [], + "app" => { "id" => "123", "name" => "example" } + }) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/123/addons/abc123" + } + + @addons.destroy end - it "removes addons with confirm option" do + it "destroys addons with confirm option" do allow(Heroku::Command).to receive(:current_options).and_return(:confirm => "example") allow(@addons).to receive(:args).and_return(%w( addon1 )) - expect(@addons.heroku).to receive(:uninstall_addon).with('example', 'addon1', :confirm => "example") - @addons.remove + allow(@addons).to receive(:get_attachments).and_return([]) + allow(@addons).to receive(:resolve_addon!).and_return({ + "id" => "abc123", + "config_vars" => [], + "app" => { "id" => "123", "name" => "example" } + }) + + allow(@addons.api).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/123/addons/abc123" + } + + @addons.destroy end describe "opening add-on docs" do @@ -454,6 +644,8 @@ module Heroku::Command before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") + require "launchy" + allow(Launchy).to receive(:open) end after(:each) do @@ -475,63 +667,44 @@ module Heroku::Command stderr, stdout = execute('addons:docs redistogo:nano') expect(stderr).to eq('') expect(stdout).to eq <<-STDOUT -Opening redistogo:nano docs... done +Opening redistogo docs... done STDOUT end - it "complains about ambiguity" do - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/addons$} - }, - { - :body => MultiJson.dump([ - { 'name' => 'qux:foo' }, - { 'name' => 'quux:bar' } - ]), - :status => 200, - } - ) - stderr, stdout = execute('addons:docs qu') - expect(stderr).to eq <<-STDERR - ! Ambiguous addon name: qu - ! Perhaps you meant `qux:foo` or `quux:bar`. -STDERR - expect(stdout).to eq('') - Excon.stubs.shift - end + it "complains when many_per_app" do + addon1 = stringify(addon.merge(name: "my_addon1", addon_service: { name: "my_service" })) + addon2 = stringify(addon.merge(name: "my_addon2", addon_service: { name: "my_service_2" })) + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([addon1, addon2]) - it "complains if no such addon exists" do - stderr, stdout = execute('addons:docs unknown') - expect(stderr).to eq <<-STDERR - ! `unknown` is not a heroku add-on. - ! See `heroku addons:list` for all available addons. -STDERR + stderr, stdout = execute('addons:docs my_service') expect(stdout).to eq('') - end - - it "suggests alternatives if addon has typo" do - stderr, stdout = execute('addons:docs redisgoto') expect(stderr).to eq <<-STDERR - ! `redisgoto` is not a heroku add-on. - ! Perhaps you meant `redistogo`. - ! See `heroku addons:list` for all available addons. + ! Multiple add-ons match "my_service". + ! Use the name of one of the add-on resources: + ! + ! - my_addon1 (my_service) + ! - my_addon2 (my_service_2) STDERR - expect(stdout).to eq('') end - it "complains if addon is not installed" do - stderr, stdout = execute('addons:open deployhooks:http') - expect(stderr).to eq <<-STDOUT - ! Addon not installed: deployhooks:http -STDOUT - expect(stdout).to eq('') + it "optimistically opens the page if nothing matches" do + Excon.stub(method: :get, path: %r(/addons/unknown)) do + { status: 404 } + end + + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: "[]", status: 200 } + end + + expect(Launchy).to receive(:open).with("https://devcenter.heroku.com/articles/unknown").and_return(Thread.new {}) + stderr, stdout = execute('addons:docs unknown') + expect(stdout).to eq "Opening unknown docs... done\n" + + Excon.stubs.shift(2) end end - describe "opening an addon" do + describe "opening an addon" do before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") @@ -551,66 +724,53 @@ module Heroku::Command end it "opens the addon if only one matches" do - api.post_addon('example', 'redistogo:nano') + addon.merge!(addon_service: { name: "redistogo:nano" }) + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([stringify(addon)]) require("launchy") - expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/redistogo:nano").and_return(Thread.new {}) + expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/#{addon[:id]}").and_return(Thread.new {}) stderr, stdout = execute('addons:open redistogo:nano') expect(stderr).to eq('') expect(stdout).to eq <<-STDOUT -Opening redistogo:nano for example... done +Opening redistogo:nano (my_addon) for example... done STDOUT end it "complains about ambiguity" do - Excon.stub( - { - :expects => 200, - :method => :get, - :path => %r{^/apps/example/addons$} - }, - { - :body => MultiJson.dump([ - { 'name' => 'deployhooks:email' }, - { 'name' => 'deployhooks:http' } - ]), - :status => 200, - } - ) + addon.merge!(addon_service: { name: "deployhooks:email" }) + email = stringify(addon.merge(name: "my_email", plan: { name: "email" })) + http = stringify(addon.merge(name: "my_http", plan: { name: "http" })) + + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([email, http]) + stderr, stdout = execute('addons:open deployhooks') expect(stderr).to eq <<-STDERR - ! Ambiguous addon name: deployhooks - ! Perhaps you meant `deployhooks:email` or `deployhooks:http`. + ! Multiple add-ons match "deployhooks". + ! Use the name of add-on resource: + ! + ! - my_email (email) + ! - my_http (http) STDERR expect(stdout).to eq('') - Excon.stubs.shift end it "complains if no such addon exists" do + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([]) stderr, stdout = execute('addons:open unknown') expect(stderr).to eq <<-STDERR - ! `unknown` is not a heroku add-on. - ! See `heroku addons:list` for all available addons. -STDERR - expect(stdout).to eq('') - end - - it "suggests alternatives if addon has typo" do - stderr, stdout = execute('addons:open redisgoto') - expect(stderr).to eq <<-STDERR - ! `redisgoto` is not a heroku add-on. - ! Perhaps you meant `redistogo`. - ! See `heroku addons:list` for all available addons. + ! Can not find add-on with "unknown" STDERR expect(stdout).to eq('') end it "complains if addon is not installed" do + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([]) stderr, stdout = execute('addons:open deployhooks:http') expect(stderr).to eq <<-STDOUT - ! Addon not installed: deployhooks:http + ! Can not find add-on with "deployhooks:http" STDOUT expect(stdout).to eq('') end end + end end diff --git a/spec/heroku/command/config_spec.rb b/spec/heroku/command/config_spec.rb index 9383be68d..ee4dfa45a 100644 --- a/spec/heroku/command/config_spec.rb +++ b/spec/heroku/command/config_spec.rb @@ -6,10 +6,15 @@ module Heroku::Command before(:each) do stub_core api.post_app("name" => "example", "stack" => "cedar") + + Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do + { body: MultiJson.dump({ 'name' => 'v1' }), status: 200 } + end end after(:each) do api.delete_app("example") + Excon.stubs.shift end it "shows all configs" do @@ -132,6 +137,13 @@ module Heroku::Command context "when more than one key is provided" do it "unsets all given keys" do + request_number = 1 + Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do |req| + response = { body: MultiJson.dump({ 'name' => "v#{request_number}" }), status: 200 } + request_number += 1 + response + end + stderr, stdout = execute("config:unset A B") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 43124154b..69af3b970 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -152,13 +152,39 @@ module Heroku::Command end context "promotion" do + include Support::Addons + it "promotes the specified database" do + resource = build_addon( + name: "walking-slowly-42", + addon_service: { name: "heroku-posgresql:ronin" }, + plan: { name: "ronin" }, + app: { id: 1, name: "example" }) + + ronin = build_attachment( + name: "HEROKU_POSTGRESQL_RONIN_URL", + app: { id: 1, name: "example" }, + addon: { id: resource[:id], name: "dreaming-ably-42" }) + + Excon.stub(method: :get, path: "/addons/#{resource[:id]}") do + { body: MultiJson.encode(resource), status: 200 } + end + + Excon.stub(method: :get, path: "/apps/example/addon-attachments/RONIN") do + { body: MultiJson.encode(ronin), status: 200 } + end + + Excon.stub(method: :post, path: "/addon-attachments") do + database = ronin.merge(name: "DATABASE") + { body: MultiJson.encode(database), status: 201 } + end + stderr, stdout = execute("pg:promote RONIN --confirm example") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Promoting HEROKU_POSTGRESQL_RONIN_URL to DATABASE_URL... done +Promoting walking-slowly-42 to DATABASE_URL on example... done STDOUT - expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://ronin_database_url") + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") end it "fails if no database is specified" do diff --git a/spec/heroku/command_spec.rb b/spec/heroku/command_spec.rb index 317af7ecc..d1f0ea38c 100644 --- a/spec/heroku/command_spec.rb +++ b/spec/heroku/command_spec.rb @@ -23,6 +23,7 @@ def to_s } describe "when the command requires confirmation" do + include Support::Addons let(:response_that_requires_confirmation) do {:status => 423, @@ -30,6 +31,16 @@ def to_s :body => 'terms of service required'} end + before do + Excon.stub(method: :post, path: %r(/apps/[^/]+/addons)) do |args| + { body: MultiJson.encode(build_addon(name: "my_addon", app: { name: "example" })), status: 201 } + end + end + + after do + Excon.stubs.shift + end + context "when the app is unknown" do context "and the user includes --confirm APP" do it "should set --app to APP and not ask for confirmation" do @@ -65,10 +76,16 @@ def to_s context "and the user includes --confirm APP" do it "should set --app to APP and not ask for confirmation" do - stub_request(:post, %r{apps/example/addons/my_addon$}). - with(:body => {:confirm => 'example'}) + addon = build_addon(name: "my_addon", app: { name: "example" }) + + Excon.stub(method: :post, path: %r(/apps/example/addons)) { |args| + expect(args[:body]).to include '"confirm":"example"' + { body: MultiJson.encode(build_addon(name: "my_addon", app: { name: "example" })), status: 201 } + } run "addons:add my_addon --confirm example" + + Excon.stubs.shift end end @@ -103,6 +120,16 @@ def to_s end describe "parsing errors" do + before do + Excon.stub(method: :post, path: %r(/apps/example/addons)) { |args| + { body: MultiJson.encode(build_addon(name: "my_addon", app: { name: "example" })), status: 201 } + } + end + + after do + Excon.stubs.shift + end + it "extracts error messages from response when available in XML" do expect(Heroku::Command.extract_error('Invalid app name')).to eq('Invalid app name') end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index adba71126..11d0618f5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -240,6 +240,7 @@ def self.error(e); end require "support/display_message_matcher" require "support/organizations_mock_helper" +require "support/addons_helper" RSpec.configure do |config| config.include DisplayMessageMatcher diff --git a/spec/support/addons_helper.rb b/spec/support/addons_helper.rb new file mode 100644 index 000000000..f4ca3f1a7 --- /dev/null +++ b/spec/support/addons_helper.rb @@ -0,0 +1,52 @@ +module Support + module Addons + def build_addon(addon={}) + addon_id = addon[:id] || SecureRandom.uuid + { + config_vars: addon.fetch(:config_vars, []), + created_at: Time.now, + id: addon_id, + name: addon[:name] || addon_name(addon[:plan][:name]), + + addon_service: { + id: SecureRandom.uuid, + }.merge(addon.fetch(:addon_service, {})), + + plan: { + id: SecureRandom.uuid, + }.merge(addon.fetch(:plan, {})), + + app: { + id: SecureRandom.uuid, + }.merge(addon.fetch(:app, {})), + + provider_id: addon[:provider_id], + updated_at: Time.now, + web_url: "https://addons-sso.heroku.com/apps/#{addon[:app][:name]}/addons/#{addon_id}" + } + end + + def build_attachment(attachment={}) + { + addon: { + id: SecureRandom.uuid, + }.merge(attachment.fetch(:addon, {})), + + app: { + id: SecureRandom.uuid, + }.merge(attachment.fetch(:app, {})), + + created_at: Time.now, + id: attachment.fetch(:id, SecureRandom.uuid), + name: attachment[:name], + updated_at: Time.now + } + end + + # Helpers generate Hashes with symbol keys. When using as outside of + # a request stub, we need them all the be strings. See "understands foo=baz". + def stringify(options) + MultiJson.decode(MultiJson.encode(options)) + end + end +end From f19f472c6a236220b60e572e4fa2c189b70a45ea Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Fri, 1 May 2015 16:26:19 -0700 Subject: [PATCH 483/952] Make upgrade/downgrade help text more accurate --- lib/heroku/command/addons.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 03a013620..240e13750 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -224,7 +224,7 @@ def detach end end - # addons:upgrade ADDON_NAME PLAN + # addons:upgrade ADDON_NAME ADDON_SERVICE:PLAN # # upgrade an existing add-on resource to PLAN # @@ -265,7 +265,7 @@ def upgrade end end - # addons:downgrade ADDON_NAME PLAN + # addons:downgrade ADDON_NAME ADDON_SERVICE:PLAN # # downgrade an existing add-on resource to PLAN # From 2d245d35d6d8854c84c4158b615597030f883d81 Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Mon, 4 May 2015 10:00:06 -0700 Subject: [PATCH 484/952] Deprecate heroku-addon-attachments --- lib/heroku/plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index c040ca911..3ca004c9c 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -8,6 +8,7 @@ class Plugin class ErrorUpdatingSymlinkPlugin < StandardError; end DEPRECATED_PLUGINS = %w( + heroku-addon-attachments heroku-cedar heroku-certs heroku-credentials From 61e33e7ae1be6dcc0a3074ad6dde04ba4ce9ca4b Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Mon, 4 May 2015 10:01:26 -0700 Subject: [PATCH 485/952] Remove switzerland variant from version --- lib/heroku/helpers/addons/api.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers/addons/api.rb b/lib/heroku/helpers/addons/api.rb index 32a1a1193..4daa35805 100644 --- a/lib/heroku/helpers/addons/api.rb +++ b/lib/heroku/helpers/addons/api.rb @@ -1,7 +1,7 @@ module Heroku::Helpers module Addons module API - VERSION="3.switzerland".freeze + VERSION="3".freeze def request(options = {}) defaults = { From 8b25164fe4879d2772eb1753206b323d99b303ee Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 4 May 2015 15:40:02 -0700 Subject: [PATCH 486/952] v3.34.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cbfd86b71..454f9276b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.34.0 2015-05-04 +================= +Pull in heroku-addon-attachments plugin + 3.33.0 2015-05-01 ================= Added shell flag to config:get diff --git a/Gemfile.lock b/Gemfile.lock index 6a5d0e68e..5c4dfd557 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.33.0) + heroku (3.34.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index dceaa1a8d..0d3508b71 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.33.0" + VERSION = "3.34.0" end From 3c13562e979686b7757fdb5968799f1590279d9d Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Tue, 5 May 2015 00:20:44 -0700 Subject: [PATCH 487/952] Fix addons:open for paranoid apps --- lib/heroku/command/addons.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 240e13750..dccecbf60 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -371,6 +371,7 @@ def open error("Usage: heroku addons:open ADDON\nMust specify ADDON to open.") end validate_arguments! + requires_preauth addon = resolve_addon!(addon_name) return addon if addon.is_a?(String) From 7bb79ec2dd8d3f2719a082b7aadabfe4c1871592 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 29 Apr 2015 15:37:59 -0700 Subject: [PATCH 488/952] added --exit-code flag for heroku run --- lib/heroku/command/run.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 868066b0b..a20948397 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -35,6 +35,10 @@ class Heroku::Command::Run < Heroku::Command::Base # ~ $ # def index + if args.include? '--exit-code' + v4_run + return + end command = args.join(" ") error("Usage: heroku run COMMAND") if command.empty? warn_if_using_jruby @@ -203,4 +207,9 @@ def console_history_add(app, cmd) File.open(console_history_file(app), "a") { |f| f.puts cmd + "\n" } end + def v4_run + Heroku::JSPlugin.setup + Heroku::JSPlugin.install('heroku-run') unless Heroku::JSPlugin.is_plugin_installed?('heroku-run') + Heroku::JSPlugin.run('_run', nil, ARGV[1..-1]) + end end From 8ab7e63d8bb98b0b428ee67e512ac4aa0b6f1ae8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 00:34:03 -0700 Subject: [PATCH 489/952] v3.35.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 454f9276b..9fecd749c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.35.0 2015-05-05 +================= +Added --exit-code flag to run command +Fixed addons:open for paranoid apps + 3.34.0 2015-05-04 ================= Pull in heroku-addon-attachments plugin diff --git a/Gemfile.lock b/Gemfile.lock index 5c4dfd557..b74ed7d4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.34.0) + heroku (3.35.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0d3508b71..4dbf4d05f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.34.0" + VERSION = "3.35.0" end From b0d696381e7f8647a84404e11c46d97762990030 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 13:07:59 -0700 Subject: [PATCH 490/952] use v4 run for anything with "--" in the arguments --- lib/heroku/command/run.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index a20948397..80f64df78 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -30,12 +30,12 @@ class Heroku::Command::Run < Heroku::Command::Base # #Example: # - # $ heroku run bash + # $ heroku run -- bash # Running `bash` attached to terminal... up, run.1 # ~ $ # def index - if args.include? '--exit-code' + if ARGV.include?('--') || ARGV.include?('--exit-code') v4_run return end From e2e075d10baf4243e05fe0368c85307afa9d696b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 13:10:01 -0700 Subject: [PATCH 491/952] renamed run command in v4 --- lib/heroku/command/run.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 80f64df78..b4c1b4325 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -210,6 +210,6 @@ def console_history_add(app, cmd) def v4_run Heroku::JSPlugin.setup Heroku::JSPlugin.install('heroku-run') unless Heroku::JSPlugin.is_plugin_installed?('heroku-run') - Heroku::JSPlugin.run('_run', nil, ARGV[1..-1]) + Heroku::JSPlugin.run('run', nil, ARGV[1..-1]) end end From cb41a4016ff26a045c32a6d8a2a1618a87671c85 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 13:16:33 -0700 Subject: [PATCH 492/952] v3.35.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9fecd749c..caf146f0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.35.1 2015-05-05 +================= +Enabled v4 version of run for commands with a '--' argument + 3.35.0 2015-05-05 ================= Added --exit-code flag to run command diff --git a/Gemfile.lock b/Gemfile.lock index b74ed7d4e..ed7a30e0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.35.0) + heroku (3.35.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4dbf4d05f..d6004871a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.35.0" + VERSION = "3.35.1" end From a7baff6484e4077ed62bd276c25528cd5d96a986 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 15:17:29 -0700 Subject: [PATCH 493/952] removed extra update message --- lib/heroku/command/update.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index ce4306b91..51bb3e1bb 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -18,7 +18,6 @@ def index validate_arguments! update_from_url(false) if Heroku::JSPlugin.setup? - display("Updating Toolbelt v4...") Heroku::JSPlugin.update end end From b26926137fb86653a1eb9498f0f55a49dcb20440 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 5 May 2015 16:24:03 -0700 Subject: [PATCH 494/952] Retry backup status polling for a while We sometimes have intermittent issues with API endpoint failures for checking backup status; we're working to fix these, but in the meantime, there's no reason to fail a command polling transfer status when this happens; just ignore the update and try again at the next polling interval. --- lib/heroku/command/pg_backups.rb | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index b3f08e461..43b2ef20f 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -402,15 +402,24 @@ def poll_transfer(action, transfer_id) # pending, running, complete--poll endpoint to get backup = nil ticks = 0 + failed_count = 0 begin - backup = hpg_app_client(app).transfers_get(transfer_id) - status = if backup[:started_at] - "Running... #{size_pretty(backup[:processed_bytes])}" - else - "Pending... #{spinner(ticks)}" - end - redisplay status - ticks += 1 + begin + backup = hpg_app_client(app).transfers_get(transfer_id) + failed_count = 0 + status = if backup[:started_at] + "Running... #{size_pretty(backup[:processed_bytes])}" + else + "Pending... #{spinner(ticks)}" + end + redisplay status + ticks += 1 + rescue RestClient::Exception + failed_count += 1 + if failed_count > 120 + raise + end + end sleep 1 end until backup[:finished_at] if backup[:succeeded] From ba586d95c6f118a7312902b1f01af076ee5631ca Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 19:58:53 -0700 Subject: [PATCH 495/952] added v4 implementation of heroku git commands --- lib/heroku/command/fork.rb | 3 +- lib/heroku/command/git.rb | 33 ++------ lib/heroku/command/local.rb | 3 +- lib/heroku/command/plugins.rb | 3 +- lib/heroku/command/run.rb | 9 +- lib/heroku/helpers.rb | 6 -- lib/heroku/jsplugin.rb | 5 +- spec/heroku/command/git_spec.rb | 145 -------------------------------- 8 files changed, 17 insertions(+), 190 deletions(-) delete mode 100644 spec/heroku/command/git_spec.rb diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index d833bdd47..889961bfd 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -16,8 +16,7 @@ class Fork < Base # --skip-pg # skip postgres databases # def index - Heroku::JSPlugin.setup - Heroku::JSPlugin.install('heroku-fork') unless Heroku::JSPlugin.is_plugin_installed?('heroku-fork') + Heroku::JSPlugin.install('heroku-fork') Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) end end diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb index ddd446338..2ccfb3b77 100644 --- a/lib/heroku/command/git.rb +++ b/lib/heroku/command/git.rb @@ -4,10 +4,11 @@ # class Heroku::Command::Git < Heroku::Command::Base - # git:clone APP [DIRECTORY] + # git:clone [DIRECTORY] # # clones a heroku app to your local machine at DIRECTORY (defaults to app name) # + # -a, --app APP # the Heroku app to use # -r, --remote REMOTE # the git remote to create, default "heroku" # --ssh-git # use SSH git protocol # --http-git # HIDDEN: Use HTTP git protocol @@ -15,20 +16,14 @@ class Heroku::Command::Git < Heroku::Command::Base # #Examples: # - # $ heroku git:clone example - # Cloning from app 'example'... + # $ heroku git:clone -a example # Cloning into 'example'... # remote: Counting objects: 42, done. # ... # def clone - name = options[:app] || shift_argument || error("Usage: heroku git:clone APP [DIRECTORY]") - directory = shift_argument - validate_arguments! - app_info = api.get_app(app).body - - puts "Cloning from app '#{name}'..." - system "git clone -o #{remote_name} #{git_url(app_info['name'])} #{directory}".strip + Heroku::JSPlugin.install('heroku-git') + Heroku::JSPlugin.run('git', 'clone', ARGV[1..-1]) end alias_command "clone", "git:clone" @@ -39,6 +34,7 @@ def clone # # if OPTIONS are specified they will be passed to git remote add # + # -a, --app APP # the Heroku app to use # -r, --remote REMOTE # the git remote to create, default "heroku" # --ssh-git # use SSH git protocol # --http-git # HIDDEN: Use HTTP git protocol @@ -46,21 +42,10 @@ def clone #Examples: # # $ heroku git:remote -a example - # Git remote heroku added + # set git remote heroku to https://git.heroku.com/example.git # def remote - validate_arguments! - app_info = api.get_app(app).body - if git('remote').split("\n").include?(remote_name) - update_git_remote(remote_name, git_url(app_info['name'])) - else - create_git_remote(remote_name, git_url(app_info['name'])) - end - end - - private - - def remote_name - options[:remote] || extract_remote_from_git_config || 'heroku' + Heroku::JSPlugin.install('heroku-git') + Heroku::JSPlugin.run('git', 'remote', ARGV[1..-1]) end end diff --git a/lib/heroku/command/local.rb b/lib/heroku/command/local.rb index b6a195a12..bcd7e7dbe 100644 --- a/lib/heroku/command/local.rb +++ b/lib/heroku/command/local.rb @@ -24,8 +24,7 @@ class Local < Base # -r, --r # def index - Heroku::JSPlugin.setup - Heroku::JSPlugin.install('heroku-local') unless Heroku::JSPlugin.is_plugin_installed?('heroku-local') + Heroku::JSPlugin.install('heroku-local') Heroku::JSPlugin.run('local', nil, ARGV[1..-1]) end end diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 7bdfe0820..363ed75cc 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -109,8 +109,7 @@ def update private def js_plugin_install(name) - Heroku::JSPlugin.setup - Heroku::JSPlugin.install(name) + Heroku::JSPlugin.install(name, force: true) end def ruby_plugin_install(name) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index b4c1b4325..92b252363 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -36,7 +36,8 @@ class Heroku::Command::Run < Heroku::Command::Base # def index if ARGV.include?('--') || ARGV.include?('--exit-code') - v4_run + Heroku::JSPlugin.install('heroku-run') + Heroku::JSPlugin.run('run', nil, ARGV[1..-1]) return end command = args.join(" ") @@ -206,10 +207,4 @@ def console_history_add(app, cmd) Readline::HISTORY.push(cmd) File.open(console_history_file(app), "a") { |f| f.puts cmd + "\n" } end - - def v4_run - Heroku::JSPlugin.setup - Heroku::JSPlugin.install('heroku-run') unless Heroku::JSPlugin.is_plugin_installed?('heroku-run') - Heroku::JSPlugin.run('run', nil, ARGV[1..-1]) - end end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 07fabbe2d..8a41bcde7 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -174,12 +174,6 @@ def create_git_remote(remote, url) display "Git remote #{remote} added" if $?.success? end - def update_git_remote(remote, url) - return unless has_git_remote? remote - git "remote set-url #{remote} #{url}" - display "Git remote #{remote} updated" if $?.success? - end - def longest(items) items.map { |i| i.to_s.length }.sort.last end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 17949a9dc..ad546ce51 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -67,8 +67,9 @@ def self.commands_info @commands_info ||= json_decode(`#{bin} commands --json`) end - def self.install(name) - system "#{bin} plugins:install #{name}" + def self.install(name, opts={}) + self.setup + system "#{bin} plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) end def self.uninstall(name) diff --git a/spec/heroku/command/git_spec.rb b/spec/heroku/command/git_spec.rb deleted file mode 100644 index cace6af4a..000000000 --- a/spec/heroku/command/git_spec.rb +++ /dev/null @@ -1,145 +0,0 @@ -require 'spec_helper' -require 'heroku/command/git' - -module Heroku::Command - describe Git do - - before(:each) do - stub_core - end - - context("clone") do - - before(:each) do - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "clones and adds remote" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku https://git.heroku.com/example.git") do - puts "Cloning into 'example'..." - end - end - stderr, stdout = execute("git:clone example") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Cloning from app 'example'... -Cloning into 'example'... - STDOUT - end - - it "clones into another dir" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku https://git.heroku.com/example.git somedir") do - puts "Cloning into 'somedir'..." - end - end - stderr, stdout = execute("git:clone example somedir") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Cloning from app 'example'... -Cloning into 'somedir'... - STDOUT - end - - it "can specify app with -a" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku https://git.heroku.com/example.git") do - puts "Cloning into 'example'..." - end - end - stderr, stdout = execute("git:clone -a example") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Cloning from app 'example'... -Cloning into 'example'... - STDOUT - end - - it "can specify app with -a and a dir" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o heroku https://git.heroku.com/example.git somedir") do - puts "Cloning into 'somedir'..." - end - end - stderr, stdout = execute("git:clone -a example somedir") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Cloning from app 'example'... -Cloning into 'somedir'... - STDOUT - end - - it "clones and sets -r remote" do - any_instance_of(Heroku::Command::Git) do |git| - mock(git).system("git clone -o other https://git.heroku.com/example.git") do - puts "Cloning into 'example'..." - end - end - stderr, stdout = execute("git:clone example -r other") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Cloning from app 'example'... -Cloning into 'example'... - STDOUT - end - - end - - context("remote") do - - before(:each) do - api.post_app("name" => "example", "stack" => "cedar") - FileUtils.mkdir('example') - FileUtils.chdir('example') { `git init` } - end - - after(:each) do - api.delete_app("example") - FileUtils.rm_rf('example') - end - - it "adds remote" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('config heroku.remote') - stub(git).git('remote').returns("origin") - stub(git).git('remote add heroku https://git.heroku.com/example.git') - end - stderr, stdout = execute("git:remote") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Git remote heroku added - STDOUT - end - - it "adds -r remote" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('remote').returns("origin") - stub(git).git('remote add other https://git.heroku.com/example.git') - end - stderr, stdout = execute("git:remote -r other") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Git remote other added - STDOUT - end - - it "updates remote when it already exists" do - any_instance_of(Heroku::Command::Git) do |git| - stub(git).git('config heroku.remote') - stub(git).git('remote').returns("heroku") - stub(git).git('remote set-url heroku https://git.heroku.com/example.git') - end - stderr, stdout = execute("git:remote") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Git remote heroku updated - STDOUT - end - end - end -end From 53d94f91f514c6363f6e45b86e9ada7fe612ba4c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 21:06:08 -0700 Subject: [PATCH 496/952] v4 version of heroku maintenance --- lib/heroku/command/maintenance.rb | 24 +++--------- spec/heroku/command/maintenance_spec.rb | 51 ------------------------- 2 files changed, 6 insertions(+), 69 deletions(-) delete mode 100644 spec/heroku/command/maintenance_spec.rb diff --git a/lib/heroku/command/maintenance.rb b/lib/heroku/command/maintenance.rb index eb7d887a4..e264f1dce 100644 --- a/lib/heroku/command/maintenance.rb +++ b/lib/heroku/command/maintenance.rb @@ -14,14 +14,8 @@ class Heroku::Command::Maintenance < Heroku::Command::Base # off # def index - validate_arguments! - - case api.get_app_maintenance(app).body['maintenance'] - when true - display('on') - when false - display('off') - end + Heroku::JSPlugin.install('heroku-apps') + Heroku::JSPlugin.run('maintenance', nil, ARGV[1..-1]) end # maintenance:on @@ -34,11 +28,8 @@ def index # Enabling maintenance mode for example # def on - validate_arguments! - - action("Enabling maintenance mode for #{app}") do - api.post_app_maintenance(app, '1') - end + Heroku::JSPlugin.install('heroku-apps') + Heroku::JSPlugin.run('maintenance', 'on', ARGV[1..-1]) end # maintenance:off @@ -51,11 +42,8 @@ def on # Disabling maintenance mode for example # def off - validate_arguments! - - action("Disabling maintenance mode for #{app}") do - api.post_app_maintenance(app, '0') - end + Heroku::JSPlugin.install('heroku-apps') + Heroku::JSPlugin.run('maintenance', 'off', ARGV[1..-1]) end end diff --git a/spec/heroku/command/maintenance_spec.rb b/spec/heroku/command/maintenance_spec.rb deleted file mode 100644 index 8eea6967d..000000000 --- a/spec/heroku/command/maintenance_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "spec_helper" -require "heroku/command/maintenance" - -module Heroku::Command - describe Maintenance do - - before(:each) do - stub_core - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "displays off for maintenance mode of an app" do - stderr, stdout = execute("maintenance") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -off -STDOUT - end - - it "displays on for maintenance mode of an app" do - api.post_app_maintenance('example', '1') - - stderr, stdout = execute("maintenance") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -on -STDOUT - end - - it "turns on maintenance mode for the app" do - stderr, stdout = execute("maintenance:on") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Enabling maintenance mode for example... done -STDOUT - end - - it "turns off maintenance mode for the app" do - stderr, stdout = execute("maintenance:off") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Disabling maintenance mode for example... done -STDOUT - end - - end -end From cafe221ba429811ba9c98d526352188aaea1faf0 Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Wed, 6 May 2015 11:32:39 -0700 Subject: [PATCH 497/952] expand hpg shorthands in addons:create --- lib/heroku/command/addons.rb | 15 ++++++++++++++- spec/heroku/command/addons_spec.rb | 11 +++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index dccecbf60..6a8e38b05 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -104,7 +104,8 @@ def create requires_preauth - service_plan = args.shift + service_plan = expand_hpg_shorthand(args.shift) + raise CommandFailed.new("Missing requested service or plan") if service_plan.nil? || %w{--fork --follow --rollback}.include?(service_plan) config = parse_options(args) @@ -386,6 +387,18 @@ def addon_docs_url(addon) "https://devcenter.#{heroku.host}/articles/#{addon.split(':').first}" end + def expand_hpg_shorthand(addon_plan) + if addon_plan =~ /\Ahpg:/ + addon_plan = "heroku-postgresql:#{addon_plan.split(':').last}" + end + if addon_plan =~ /\Aheroku-postgresql:[spe]\d+\z/ + addon_plan.gsub!(/:s/,':standard-') + addon_plan.gsub!(/:p/,':premium-') + addon_plan.gsub!(/:e/,':enterprise-') + end + addon_plan + end + #this will clean up when we officially deprecate def parse_options(args) config = {} diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index fdfb52e0b..0d4ba0610 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -348,6 +348,17 @@ module Heroku::Command @addons.create end + it "expands hgp:s0 to heroku-postgresql:standard-0" do + allow(@addons).to receive(:args).and_return(%w(hpg:s0)) + + allow(@addons).to receive(:request) { |args| + expect(args[:path]).to eq "/apps/example/addons" + expect(args[:body]).to include '"name":"heroku-postgresql:standard-0"' + }.and_return(stringify(addon)) + + @addons.create + end + it "adds an addon with a price" do Excon.stub(method: :post, path: %r(/apps/example/addons)) do addon = build_addon( From a00e7bb50074b15895236ca8bbd1fc319468b374 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 6 May 2015 12:16:29 -0700 Subject: [PATCH 498/952] soften release update warnings For non-autoupdateable clients, only show the update warning if the minor version is out of date. Also, use the autoupdate file to only show the message once in a while. --- lib/heroku/updater.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index ea161b6cf..4835e6b6a 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -50,6 +50,10 @@ def self.needs_update? compare_versions(latest_version, latest_local_version) > 0 end + def self.needs_minor_update? + latest_version[0..3] != latest_local_version[0..3] + end + def self.client_version_from_path(path) version_file = File.join(path, "lib/heroku/version.rb") if File.exists?(version_file) @@ -87,18 +91,18 @@ def self.wait_for_lock(wait_for=5, check_every=0.5) end def self.autoupdate - return warn_if_out_of_date if disable # if we've updated in the last hour, don't try again if File.exists?(last_autoupdate_path) return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60 end FileUtils.mkdir_p File.dirname(last_autoupdate_path) FileUtils.touch last_autoupdate_path + return warn_if_out_of_date if disable update end def self.warn_if_out_of_date - $stderr.puts "WARNING: Toolbelt v#{latest_version} update available." if needs_update? + $stderr.puts "WARNING: Toolbelt v#{latest_version} update available." if needs_minor_update? end def self.update(prerelease=false) From 54dc9db6b8b0d69f44003a946ce1b8c5b19bc823 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 6 May 2015 12:27:25 -0700 Subject: [PATCH 499/952] v3.36.0 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index caf146f0a..9fd5eedbf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.36.0 2015-05-06 +================= +Included hpg addon:create shortcuts from heroku-pg-extras +Add quota indicator for ps +Show release update warnings less frequently +Switch to v4 version of maintenance commands +Switch to v4 version of git commands + 3.35.1 2015-05-05 ================= Enabled v4 version of run for commands with a '--' argument diff --git a/Gemfile.lock b/Gemfile.lock index ed7a30e0b..f3455dd25 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.35.1) + heroku (3.36.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d6004871a..97ab24f8d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.35.1" + VERSION = "3.36.0" end From c26e8b6f57bcf8b7cdbc87329c6d9e66f7ff6d1b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 6 May 2015 16:21:36 -0700 Subject: [PATCH 500/952] added note that process types must be alphanumeric --- lib/heroku/command/ps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 49da3cee5..3ef96e195 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -238,7 +238,7 @@ def scale end.compact if changes.empty? - error("Usage: heroku ps:scale DYNO1=AMOUNT1[:SIZE] [DYNO2=AMOUNT2 ...]\nMust specify DYNO and AMOUNT to scale.") + error("Usage: heroku ps:scale DYNO1=AMOUNT1[:SIZE] [DYNO2=AMOUNT2 ...]\nMust specify DYNO and AMOUNT to scale.\nDYNO must be alphanumeric.") end action("Scaling dynos") do From 05310b4df2355309a86f86cf5925b4d76d0594ed Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 5 May 2015 08:38:32 -0700 Subject: [PATCH 501/952] Fix unscheduling backups for some config var configurations For some applications with aliased config vars, it's possible that the unschedule command can be too stringent in verifying the database to unschedule, making it impossible to unschedule backups without manually calling the API. This makes the unschedule verification much simpler. --- lib/heroku/command/pg_backups.rb | 9 ++++--- spec/heroku/command/pg_backups_spec.rb | 34 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index b3f08e461..5ba95a60f 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -513,16 +513,19 @@ def unschedule_backups db = shift_argument validate_arguments! + if db.nil? + abort("Must specify database to unschedule backups for") + end + attachment = generate_resolver.resolve(db, "DATABASE_URL") schedule = hpg_client(attachment).schedules.find do |s| - # attachment.name is HEROKU_POSTGRESQL_COLOR # s[:name] is HEROKU_POSTGRESQL_COLOR_URL - s[:name] =~ /#{attachment.name}/ || attachment.name =~ /#{s[:name]}/ + s[:name] =~ /#{db}/i end if schedule.nil? - display "No automatic daily backups for #{attachment.name} found" + display "No automatic daily backups for #{db || attachment.name} found" else hpg_client(attachment).unschedule(schedule[:uuid]) display "Stopped automatic daily backups for #{attachment.name}" diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 7e00fb3d6..77c0f96ae 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -122,6 +122,40 @@ module Heroku::Command end end + describe "heroku pg:backups unschedule" do + let(:schedules) do + [ { name: 'HEROKU_POSTGRESQL_GREEN_URL', + uuid: 'ffffffff-ffff-ffff-ffff-ffffffffffff' }, + { name: 'DATABASE_URL', + uuid: 'ffffffff-ffff-ffff-ffff-fffffffffffe' } ] + end + + before do + stub_pg.schedules.returns(schedules) + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + end + + it "unschedules the specified backup" do + stub_pg.unschedule + stderr, stdout = execute("pg:backups unschedule green --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/Stopped automatic daily backups for/) + end + + it "complains when called without an argument" do + stderr, stdout = execute("pg:backups unschedule --confirm example") + expect(stderr).to match(/Must specify database to unschedule/) + expect(stdout).to be_empty + end + + it "indicates when no matching backup can be unscheduled" do + stderr, stdout = execute("pg:backups unschedule red --confirm example") + expect(stderr).to be_empty + expect(stdout).to match(/No automatic daily backups for/) + end + end + describe "heroku pg:backups" do let(:logged_at) { Time.now } let(:started_at) { Time.now } From 05f3e61cf15b1756017d43f40a17df8a13df4673 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Wed, 6 May 2015 17:56:26 -0700 Subject: [PATCH 502/952] Remove pointless case in error message --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 5ba95a60f..b3552c3b1 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -525,7 +525,7 @@ def unschedule_backups end if schedule.nil? - display "No automatic daily backups for #{db || attachment.name} found" + display "No automatic daily backups for #{attachment.name} found" else hpg_client(attachment).unschedule(schedule[:uuid]) display "Stopped automatic daily backups for #{attachment.name}" From 1bc88430ccd9e92fadd864e1679f880d38771d23 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 6 May 2015 22:25:11 -0700 Subject: [PATCH 503/952] load rexml on demand --- lib/heroku/client.rb | 2 +- lib/heroku/command.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 0bc6618ed..88d3f7807 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -1,4 +1,3 @@ -require 'rexml/document' require 'uri' require 'time' require 'heroku/auth' @@ -654,6 +653,7 @@ def heroku_headers # :nodoc: end def xml(raw) # :nodoc: + require 'rexml/document' REXML::Document.new(raw) end diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 3d7bb00f4..7fe54d1e4 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -298,10 +298,11 @@ def self.parse(cmd) def self.extract_error(body, options={}) default_error = block_given? ? yield : "Internal server error.\nRun `heroku status` to check for known platform issues." - parse_error_xml(body) || parse_error_json(body) || parse_error_plain(body) || default_error + parse_error_json(body) || parse_error_xml(body) || parse_error_plain(body) || default_error end def self.parse_error_xml(body) + require 'rexml/document' xml_errors = REXML::Document.new(body).elements.to_a("//errors/error") msg = xml_errors.map { |a| a.text }.join(" / ") return msg unless msg.empty? From 523a45ee62d97ec65602b4b4ea10c0419455a9f4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 6 May 2015 23:31:34 -0700 Subject: [PATCH 504/952] v4 takeover This allows the v4 CLI to have the first chance at running a command before v3. I also optimized the load order by only loading the necessary ruby files before this happens. --- lib/heroku.rb | 1 - lib/heroku/cli.rb | 18 ++++-------------- lib/heroku/client.rb | 1 - lib/heroku/command.rb | 7 +++++-- lib/heroku/jsplugin.rb | 11 +++++++++++ spec/spec_helper.rb | 1 + 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/heroku.rb b/lib/heroku.rb index d35bcca34..a01417932 100644 --- a/lib/heroku.rb +++ b/lib/heroku.rb @@ -1,4 +1,3 @@ -require "heroku/client" require "heroku/updater" require "heroku/version" diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index c5a523526..370f03a1e 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -6,22 +6,10 @@ load('heroku/helpers.rb') # reload helpers after possible inject_loadpath load('heroku/updater.rb') # reload updater after possible inject_loadpath -# exists and updated in the last 5 minutes -if File.exist?(Heroku::Updater.updating_lock_path) && - File.mtime(Heroku::Updater.updating_lock_path) > (Time.now - 5*60) - $stderr.puts "Heroku Toolbelt is currently updating. Please wait a few seconds and try your command again." - exit 1 -end - require 'heroku' -require 'heroku/command' -require 'heroku/git' -require 'heroku/helpers' -require 'heroku/http_instrumentor' +require 'heroku/jsplugin' require 'heroku/rollbar' -require 'rest_client' require 'multi_json' -require 'heroku-api' begin # attempt to load the JSON parser bundled with ruby for multi_json @@ -39,8 +27,10 @@ class Heroku::CLI def self.start(*args) $stdin.sync = true if $stdin.isatty $stdout.sync = true if $stdout.isatty - Heroku::Git.check_git_version command = args.shift.strip rescue "help" + Heroku::JSPlugin.try_takeover(command, args) if Heroku::JSPlugin.setup? + require 'heroku/command' + Heroku::Git.check_git_version Heroku::Command.load Heroku::Command.run(command, args) Heroku::Updater.autoupdate diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 88d3f7807..81e1871f5 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -5,7 +5,6 @@ require 'heroku/helpers' require 'heroku/version' require 'heroku/client/ssl_endpoint' -require 'rest_client' # A Ruby class to call the Heroku REST API. You might use this if you want to # manage your Heroku apps from within a Ruby program, such as Capistrano. diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 7fe54d1e4..d62c74a42 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -1,8 +1,11 @@ require 'heroku/helpers' require 'heroku/plugin' -require 'heroku/jsplugin' require 'heroku/version' -require "optparse" +require 'heroku/http_instrumentor' +require 'heroku/git' +require 'heroku-api' +require 'optparse' +require 'rest_client' module Heroku module Command diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index ad546ce51..a328feb19 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -5,6 +5,17 @@ def self.setup? @is_setup ||= File.exists? bin end + def self.try_takeover(command, args) + command = command.split(':') + if command.length == 1 + command = commands.find { |t| t["topic"] == command[0] && t["command"] == nil } + else + command = commands.find { |t| t["topic"] == command[0] && t["command"] == command[1] } + end + return if !command || command["hidden"] + run(command['topic'], command['command'], ARGV[1..-1]) + end + def self.load! return unless setup? this = self diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 11d0618f5..e92d25ae6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,6 +8,7 @@ require "excon" require "heroku/cli" +require "heroku/client" require "rspec" require "rr" require "fakefs/safe" From f8abf8e563362e1785f2e691f96e8ac1b13116fd Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 6 May 2015 23:56:43 -0700 Subject: [PATCH 505/952] hide hidden commands from help --- lib/heroku/command/help.rb | 1 + lib/heroku/jsplugin.rb | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/help.rb b/lib/heroku/command/help.rb index 89e8b14cc..d3488984d 100644 --- a/lib/heroku/command/help.rb +++ b/lib/heroku/command/help.rb @@ -96,6 +96,7 @@ def skip_namespace?(ns) def skip_command?(command) return true if command[:help] =~ /DEPRECATED:/ return true if command[:help] =~ /^ HIDDEN:/ + return true if command[:hidden] false end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index a328feb19..865c382e3 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -43,7 +43,8 @@ def initialize(args, opts) :method => :run, :banner => plugin['usage'], :summary => " #{plugin['description']}", - :help => help + :help => help, + :hidden => plugin['hidden'], ) end end From bf51b36adb80794dca6c4ae7fd0bc976ededbb9d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 00:21:19 -0700 Subject: [PATCH 506/952] use basic json library for normal json tasks move multi_json loading to command.rb since that happens after v4 has attempted to load --- lib/heroku/cli.rb | 11 +---------- lib/heroku/command.rb | 1 + lib/heroku/helpers.rb | 8 +++----- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 370f03a1e..fd6b5c3b4 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -9,16 +9,7 @@ require 'heroku' require 'heroku/jsplugin' require 'heroku/rollbar' -require 'multi_json' - -begin - # attempt to load the JSON parser bundled with ruby for multi_json - # we're doing this because several users apparently have gems broken - # due to OS upgrades. see: https://github.com/heroku/heroku/issues/932 - require 'json' -rescue LoadError - # let multi_json fallback to yajl/oj/okjson -end +require 'json' class Heroku::CLI diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index d62c74a42..bc99161fa 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -6,6 +6,7 @@ require 'heroku-api' require 'optparse' require 'rest_client' +require 'multi_json' module Heroku module Command diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 579062f22..74f87ad6c 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -212,14 +212,12 @@ def display_row(row, lengths) end def json_encode(object) - MultiJson.dump(object) - rescue MultiJson::ParseError - nil + JSON.generate(object) end def json_decode(json) - MultiJson.load(json) - rescue MultiJson::ParseError + JSON.parse(json) + rescue JSON::ParserError nil end From 2c20229288159a3b31c723c19c410101a304383d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 09:29:33 -0700 Subject: [PATCH 507/952] added debug info for running v4 commands --- lib/heroku/jsplugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 865c382e3..b51c65f57 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -123,6 +123,7 @@ def self.setup def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic + debug("running #{cmd} on v4") exec self.bin, cmd, *args end From 4097233480f002f846ed881e899c434b26eeecf1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 10:02:20 -0700 Subject: [PATCH 508/952] v3.36.1 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9fd5eedbf..66c09a210 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.36.1 2015-05-07 +================= +Big performance boost to v4 commands by running them before v3 is setup +Optimize ruby require order + 3.36.0 2015-05-06 ================= Included hpg addon:create shortcuts from heroku-pg-extras diff --git a/Gemfile.lock b/Gemfile.lock index f3455dd25..5cdca210e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.0) + heroku (3.36.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 97ab24f8d..8811cf7f2 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.0" + VERSION = "3.36.1" end From 3a2dc8002dd6a652998bf33974189c4c03c76ba5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 11:28:49 -0700 Subject: [PATCH 509/952] show warning if toolbelt is updating --- lib/heroku/cli.rb | 1 + lib/heroku/updater.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index fd6b5c3b4..ec144dfc6 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -18,6 +18,7 @@ class Heroku::CLI def self.start(*args) $stdin.sync = true if $stdin.isatty $stdout.sync = true if $stdout.isatty + Heroku::Updater.warn_if_updating command = args.shift.strip rescue "help" Heroku::JSPlugin.try_takeover(command, args) if Heroku::JSPlugin.setup? require 'heroku/command' diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 4835e6b6a..9e96c59d4 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -182,5 +182,9 @@ def self.inject_libpath def self.last_autoupdate_path File.join(Heroku::Helpers.home_directory, ".heroku", "autoupdate.last") end + + def self.warn_if_updating + warn "WARNING: Toolbelt is currently updating" if File.exists?(updating_lock_path) + end end end From 5ba18a6680289315d295a6693216d3964eeddf8b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 12:37:43 -0700 Subject: [PATCH 510/952] fixed issue on windows with usernames that have spaces --- lib/heroku/jsplugin.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index b51c65f57..a1eacfca3 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -51,7 +51,7 @@ def initialize(args, opts) def self.plugins return [] unless setup? - @plugins ||= `#{bin} plugins`.lines.map do |line| + @plugins ||= `"#{bin}" plugins`.lines.map do |line| name, version = line.split { :name => name, :version => version } end @@ -76,24 +76,24 @@ def self.commands end def self.commands_info - @commands_info ||= json_decode(`#{bin} commands --json`) + @commands_info ||= json_decode(`"#{bin}" commands --json`) end def self.install(name, opts={}) self.setup - system "#{bin} plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) + system "\"#{bin}\" plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) end def self.uninstall(name) - system "#{bin} plugins:uninstall #{name}" + system "\"#{bin}\" plugins:uninstall #{name}" end def self.update - system "#{bin} update" + system "\"#{bin}\" update" end def self.version - `#{bin} version` + `"#{bin}" version` end def self.bin From a527320368a61726d79146fe36719eab98cf4a6f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 12:42:20 -0700 Subject: [PATCH 511/952] added documentation for new run functionality --- lib/heroku/command/run.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 92b252363..fffa19d40 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -27,13 +27,17 @@ class Heroku::Command::Run < Heroku::Command::Base # run an attached dyno # # -s, --size SIZE # specify dyno size + # --exit-code # return exit code from process # #Example: # - # $ heroku run -- bash + # $ heroku run bash # Running `bash` attached to terminal... up, run.1 # ~ $ # + # $ heroku run -s hobby -- myscript.sh -a arg1 -s arg2 + # Running `myscript.sh -a arg1 -s arg2` attached to terminal... up, run.1 + # def index if ARGV.include?('--') || ARGV.include?('--exit-code') Heroku::JSPlugin.install('heroku-run') From 99a0a07052bde22718b840e78812dfbaa249c78a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 12:45:50 -0700 Subject: [PATCH 512/952] v3.36.2 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 66c09a210..a1e9c76c0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.36.2 2015-05-07 +================= +Documented new run functionality +Fixed issue on windows with spaces in username +Show warning if Toolbelt is currently updating + 3.36.1 2015-05-07 ================= Big performance boost to v4 commands by running them before v3 is setup diff --git a/Gemfile.lock b/Gemfile.lock index 5cdca210e..2bc2dd950 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.1) + heroku (3.36.2) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 8811cf7f2..ffc00211c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.1" + VERSION = "3.36.2" end From 781cf94799646c2f6fed26e4298db97e3af72dfc Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 19:32:19 -0700 Subject: [PATCH 513/952] autofix v4 installs some users have unfortunately downloaded v4 clients that are not autoupdating. This will simply remove v4 if it is causing an issue. It will automatically be redownloaded if it is needed. --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index a1eacfca3..95bd9e732 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -72,6 +72,8 @@ def self.commands commands_info['commands'] rescue $stderr.puts "error loading plugin commands" + # Remove v4 if it is causing issues (for now) + File.delete(bin) return [] end From 336ea530a4f7561d7fdea2384093ae1a71af63b3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 19:35:16 -0700 Subject: [PATCH 514/952] v3.36.3 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a1e9c76c0..eb14c1caf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.36.3 2015-05-07 +================= +Autofix broken v4 installs +More robust backup polling + 3.36.2 2015-05-07 ================= Documented new run functionality diff --git a/Gemfile.lock b/Gemfile.lock index 2bc2dd950..98e16ce83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.2) + heroku (3.36.3) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index ffc00211c..0eda3325b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.2" + VERSION = "3.36.3" end From 2580c54ee403c9feeca81fe6677cf8b64050bfc2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 19:39:15 -0700 Subject: [PATCH 515/952] safer delete --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 95bd9e732..87f2a3de3 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -73,7 +73,7 @@ def self.commands rescue $stderr.puts "error loading plugin commands" # Remove v4 if it is causing issues (for now) - File.delete(bin) + File.delete(bin) rescue nil return [] end From 12961f4bb147e88e7dec68d5b850eb9b9d758f11 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 7 May 2015 19:40:17 -0700 Subject: [PATCH 516/952] v3.36.4 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index eb14c1caf..bb7fde4eb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.36.4 2015-05-07 +================= +Made v4 autofix safer + 3.36.3 2015-05-07 ================= Autofix broken v4 installs diff --git a/Gemfile.lock b/Gemfile.lock index 98e16ce83..127d64343 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.3) + heroku (3.36.4) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0eda3325b..3033f834b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.3" + VERSION = "3.36.4" end From 96c181094ffccc40c3c7cb34d5ea6725a933104d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Thu, 7 May 2015 20:53:52 -0700 Subject: [PATCH 517/952] Add a redis shim. --- lib/heroku/command/redis.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 lib/heroku/command/redis.rb diff --git a/lib/heroku/command/redis.rb b/lib/heroku/command/redis.rb new file mode 100644 index 000000000..390ba74e5 --- /dev/null +++ b/lib/heroku/command/redis.rb @@ -0,0 +1,19 @@ +require "heroku/command/base" + +module Heroku::Command + + # list redis databases for an app + # + class Redis < Base + + # redis [DATABASE] + # + # Get information about redis database + # + # + def index + Heroku::JSPlugin.install('heroku-redis') + Heroku::JSPlugin.run('redis:info', nil, ARGV[1..-1]) + end + end +end From ccf39513afdaa5ee70356f74a0d0984963836aba Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 8 May 2015 15:47:32 -0700 Subject: [PATCH 518/952] removed usage tracking --- lib/heroku/command.rb | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index bc99161fa..f1148b5c1 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -188,26 +188,11 @@ def self.prepare_run(cmd, args=[]) @invalid_arguments = invalid_options @anonymous_command = [ARGV.first, *@anonymized_args].join(' ') - begin - usage_directory = "#{home_directory}/.heroku/usage" - FileUtils.mkdir_p(usage_directory) - usage_file = usage_directory << "/#{Heroku::VERSION}" - usage = if File.exists?(usage_file) - json_decode(File.read(usage_file)) - else - {} - end - usage[@anonymous_command] ||= 0 - usage[@anonymous_command] += 1 - File.write(usage_file, json_encode(usage) + "\n") - rescue - # usage writing is not important, allow failures - end if command command_instance = command[:klass].new(args.dup, opts.dup) - if !@normalized_args.include?('--app _') && (implied_app = command_instance.app rescue nil) + if !@normalized_args.include?('--app _') && (command_instance.app rescue nil) @normalized_args << '--app _' end @normalized_command = [ARGV.first, @normalized_args.sort_by {|arg| arg.gsub('-', '')}].join(' ') From d3fde5d7e01c8a606558216840ef91ecf81a2251 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 8 May 2015 15:47:43 -0700 Subject: [PATCH 519/952] fixed lint warning --- lib/heroku/command.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index f1148b5c1..45258c5d1 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -151,7 +151,7 @@ def self.prepare_run(cmd, args=[]) opts = {} invalid_options = [] - parser = OptionParser.new do |parser| + p = OptionParser.new do |parser| # remove OptionParsers Officious['version'] to avoid conflicts # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814 parser.base.long.delete('version') @@ -169,7 +169,7 @@ def self.prepare_run(cmd, args=[]) end begin - parser.order!(args) do |nonopt| + p.order!(args) do |nonopt| invalid_options << nonopt @anonymized_args << '!' @normalized_args << '!' From 7ed886200a0da0040701a7e3be15a71e6214e14f Mon Sep 17 00:00:00 2001 From: Kara Louie Date: Fri, 8 May 2015 16:05:16 -0700 Subject: [PATCH 520/952] Bug fixes for users with partial privileges on apps:info --- lib/heroku/command/apps.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index c21881216..63e7be7ab 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -103,7 +103,11 @@ def info styled_header(app_data["name"]) end - addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort + begin + addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort + rescue + addons_data = {} + end collaborators_data = api.get_collaborators(app).body.map {|collaborator| collaborator["email"]}.sort collaborators_data.reject! {|email| email == app_data["owner_email"]} From 7b33bedbcf1951c1049a8a330d6cc25a10503d1b Mon Sep 17 00:00:00 2001 From: Kara Louie Date: Fri, 8 May 2015 16:15:36 -0700 Subject: [PATCH 521/952] Refactor to one line --- lib/heroku/command/apps.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 63e7be7ab..66c9b022c 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -103,11 +103,7 @@ def info styled_header(app_data["name"]) end - begin - addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort - rescue - addons_data = {} - end + addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort rescue {} collaborators_data = api.get_collaborators(app).body.map {|collaborator| collaborator["email"]}.sort collaborators_data.reject! {|email| email == app_data["owner_email"]} From 0386eee76d04cd61343b3544b8b2943df33ecaf3 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 11 May 2015 09:05:41 -0700 Subject: [PATCH 522/952] More helpful unschedule error message --- lib/heroku/command/pg_backups.rb | 13 +++++++++++-- spec/heroku/command/pg_backups_spec.rb | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index b3552c3b1..22b7a9849 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -44,7 +44,7 @@ def copy # delete BACKUP_ID # delete an existing backup # schedule DATABASE # schedule nightly backups for given database # --at ':00 ' # at a specific (24h clock) hour in the given timezone - # unschedule DATABASE # stop nightly backup for database + # unschedule SCHEDULE # stop nightly backup for database # schedules # list backup schedule def backups if args.count == 0 @@ -514,7 +514,16 @@ def unschedule_backups validate_arguments! if db.nil? - abort("Must specify database to unschedule backups for") + # try to provide a more informative error message, but rescue to + # a generic error message in case things go poorly + begin + attachment = arbitrary_app_db + schedules = hpg_client(attachment).schedules + schedule_names = schedules.map { |s| s[:name] }.join(", ") + abort("Must specify schedule to cancel: existing schedules are #{schedule_names}") + rescue StandardError + abort("Must specify schedule to cancel") + end end attachment = generate_resolver.resolve(db, "DATABASE_URL") diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 77c0f96ae..ee62208e4 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -145,7 +145,7 @@ module Heroku::Command it "complains when called without an argument" do stderr, stdout = execute("pg:backups unschedule --confirm example") - expect(stderr).to match(/Must specify database to unschedule/) + expect(stderr).to match(/Must specify schedule to cancel/) expect(stdout).to be_empty end From c73000a666be3e5371208643f87465e8f337aa92 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 11 May 2015 09:10:50 -0700 Subject: [PATCH 523/952] Tweak error message and help string --- lib/heroku/command/pg_backups.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 22b7a9849..1726ee2ad 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -44,7 +44,7 @@ def copy # delete BACKUP_ID # delete an existing backup # schedule DATABASE # schedule nightly backups for given database # --at ':00 ' # at a specific (24h clock) hour in the given timezone - # unschedule SCHEDULE # stop nightly backup for database + # unschedule SCHEDULE # stop nightly backups on this schedule # schedules # list backup schedule def backups if args.count == 0 @@ -522,7 +522,7 @@ def unschedule_backups schedule_names = schedules.map { |s| s[:name] }.join(", ") abort("Must specify schedule to cancel: existing schedules are #{schedule_names}") rescue StandardError - abort("Must specify schedule to cancel") + abort("Must specify schedule to cancel. Run `heroku help pg:backups` for usage information.") end end From d02e9cfc2a04b94b9b2ecf2d8b1b5ea77199b6fb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 11 May 2015 17:26:20 -0700 Subject: [PATCH 524/952] added shim for plugins:link --- lib/heroku/command/plugins.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 363ed75cc..68ab39f80 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -106,6 +106,19 @@ def update end end + # heroku plugins:link [PATH] + # Links a local plugin into CLI. + # This is useful when developing plugins locally. + # It simply symlinks the specified path into ~/.heroku/node_modules + + #Example: + # $ heroku plugins:link . + # + def link + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('plugins', 'link', ARGV[1..-1]) + end + private def js_plugin_install(name) From 1f865b298e4ba43fd70b3b919a402a09ca56a232 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 11 May 2015 18:15:32 -0700 Subject: [PATCH 525/952] v3.36.5 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bb7fde4eb..6c3c9e700 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.36.5 2015-05-11 +================= +Added redis commands +Added plugins:link v4 shim +Bug fixe for users with partial privileges on apps:info +Removed usage tracking + 3.36.4 2015-05-07 ================= Made v4 autofix safer diff --git a/Gemfile.lock b/Gemfile.lock index 127d64343..349f19061 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.4) + heroku (3.36.5) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3033f834b..864f0f64b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.4" + VERSION = "3.36.5" end From 127f575427099d09935bcf8aa7a91d33575cda5a Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Tue, 12 May 2015 16:45:00 +1000 Subject: [PATCH 526/952] pg:promote with URL var --- lib/heroku/command/pg.rb | 1 + spec/heroku/command/pg_spec.rb | 38 +++++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 4fce2e24c..edf30f992 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -85,6 +85,7 @@ def promote end validate_arguments! + db = db.sub(/_URL$/, '') # allow promoting with a var name addon = resolve_addon!(db) attachment_name = 'DATABASE' diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 69af3b970..3fc7d44a8 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -154,7 +154,7 @@ module Heroku::Command context "promotion" do include Support::Addons - it "promotes the specified database" do + before do resource = build_addon( name: "walking-slowly-42", addon_service: { name: "heroku-posgresql:ronin" }, @@ -162,7 +162,7 @@ module Heroku::Command app: { id: 1, name: "example" }) ronin = build_attachment( - name: "HEROKU_POSTGRESQL_RONIN_URL", + name: "HEROKU_POSTGRESQL_RONIN", app: { id: 1, name: "example" }, addon: { id: resource[:id], name: "dreaming-ably-42" }) @@ -170,15 +170,47 @@ module Heroku::Command { body: MultiJson.encode(resource), status: 200 } end - Excon.stub(method: :get, path: "/apps/example/addon-attachments/RONIN") do + Excon.stub(method: :get, path: "/addons/#{resource[:name]}") do + { body: MultiJson.encode(resource), status: 200 } + end + + Excon.stub(method: :get, path: "/apps/example/addon-attachments/HEROKU_POSTGRESQL_RONIN") do { body: MultiJson.encode(ronin), status: 200 } end + Excon.stub(method: :get, path: "/apps/example/addon-attachments/RONIN") do + { body: MultiJson.encode({}), status: 404 } + end + + Excon.stub(method: :get, path: "/apps/example/addon-attachments") do + { body: MultiJson.encode([ronin]), status: 200 } + end + Excon.stub(method: :post, path: "/addon-attachments") do database = ronin.merge(name: "DATABASE") { body: MultiJson.encode(database), status: 201 } end + end + + it "promotes the specified database resource name" do + stderr, stdout = execute("pg:promote walking-slowly-42 --confirm example") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Promoting walking-slowly-42 to DATABASE_URL on example... done +STDOUT + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") + end + + it "promotes the specified database by config var" do + stderr, stdout = execute("pg:promote HEROKU_POSTGRESQL_RONIN_URL --confirm example") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Promoting walking-slowly-42 to DATABASE_URL on example... done +STDOUT + expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") + end + it "promotes the specified database by attachment substring" do stderr, stdout = execute("pg:promote RONIN --confirm example") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT From 0a07f271e789cf1c4f68d26d38ddf85266b6c540 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Tue, 12 May 2015 17:16:30 +1000 Subject: [PATCH 527/952] Ensure there is a backup promotion for old DATABASE when promoting --- lib/heroku/command/pg.rb | 54 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index edf30f992..1e5846c76 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -88,14 +88,26 @@ def promote db = db.sub(/_URL$/, '') # allow promoting with a var name addon = resolve_addon!(db) - attachment_name = 'DATABASE' - action "Promoting #{addon['name']} to #{attachment_name}_URL on #{app}" do + promoted_name = 'DATABASE' + + action "Ensuring an alternate alias for existing #{promoted_name}" do + backup = find_or_create_non_database_attachment(app) + + if backup + @status = backup['name'] + else + @status = "not needed" + end + + end + + action "Promoting #{addon['name']} to #{promoted_name}_URL on #{app}" do request( :body => json_encode({ "app" => {"name" => app}, "addon" => {"name" => addon['name']}, "confirm" => app, - "name" => attachment_name + "name" => promoted_name }), :expects => 201, :method => :post, @@ -668,4 +680,40 @@ def psql_cmd # but windows doesn't have the command command running_on_windows? ? 'psql' : 'command psql' end + + # Finds or creates a non-DATABASE attachment for the DB currently + # attached as DATABASE. + # + # If current DATABASE is attached by other names, return one of them. + # If current DATABASE is only attachment, create a new one and return it. + # If no current DATABASE, return nil. + def find_or_create_non_database_attachment(app) + attachments = get_attachments(:app => app) + + current_attachment = attachments.detect { |att| att['name'] == 'DATABASE' } + current_addon = current_attachment && current_attachment['addon'] + + if current_addon + existing = attachments. + select { |att| att['addon']['id'] == current_addon['id'] }. + detect { |att| att['name'] != 'DATABASE' } + + return existing if existing + + # The current add-on occupying the DATABASE attachment has no + # other attachments. In order to promote this database without + # error, we can create a secondary attachment, just-in-time. + request( + # Note: no attachment name provided; let the API choose one + :body => json_encode({ + "app" => {"name" => app}, + "addon" => {"name" => current_addon['name']}, + "confirm" => app + }), + :expects => 201, + :method => :post, + :path => "/addon-attachments" + ) + end + end end From 9ea0057b17b3ac096e91752bc8927e9001a86400 Mon Sep 17 00:00:00 2001 From: Michael Baudino Date: Tue, 12 May 2015 13:17:51 +0200 Subject: [PATCH 528/952] Fix plugins command help output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR removes the redundant `heroku` word here (in "Additional commands" section): ``` → heroku help plugins Usage: heroku plugins list installed plugins Example: $ heroku plugins === Installed Plugins heroku-production-check@0.2.0 Additional commands, type "heroku help COMMAND" for more details: heroku plugins:link [PATH] # This is useful when developing plugins locally. plugins:install URL # install a plugin plugins:uninstall PLUGIN # uninstall a plugin plugins:update [PLUGIN] # updates all plugins or a single plugin by name ``` And also, here: ``` → heroku help plugins:link Usage: heroku heroku plugins:link [PATH] Links a local plugin into CLI. This is useful when developing plugins locally. It simply symlinks the specified path into ~/.heroku/node_modules Example: $ heroku plugins:link . ``` --- lib/heroku/command/plugins.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 68ab39f80..0823159c8 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -106,7 +106,7 @@ def update end end - # heroku plugins:link [PATH] + # plugins:link [PATH] # Links a local plugin into CLI. # This is useful when developing plugins locally. # It simply symlinks the specified path into ~/.heroku/node_modules From 4755216ce783fa84e7479e9dd6bd83c0f06f557f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 12 May 2015 10:38:06 -0700 Subject: [PATCH 529/952] ensure that polling has a value for backup in exception tracker --- lib/heroku/command/pg_backups.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 43b2ef20f..5df66a9f3 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -415,6 +415,7 @@ def poll_transfer(action, transfer_id) redisplay status ticks += 1 rescue RestClient::Exception + backup = {} failed_count += 1 if failed_count > 120 raise From dac1474c8877938795036edebba26a0bc2610463 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 12 May 2015 12:36:05 -0700 Subject: [PATCH 530/952] fixed rbconfig on ruby 1.9.2 --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 87f2a3de3..1ab592ee2 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -1,3 +1,5 @@ +require 'rbconfig' + class Heroku::JSPlugin extend Heroku::Helpers From 1347a7042ed25c5a8d7abe87a64c14c1797a0d57 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 12 May 2015 12:57:27 -0700 Subject: [PATCH 531/952] v3.36.5 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6c3c9e700..547f19bf1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.36.6 2015-05-12 +================= +Fixed bug with pgbackups polling +Fixed plugin command help +Fixed rbconfig bug on ruby 1.9.2 + 3.36.5 2015-05-11 ================= Added redis commands diff --git a/Gemfile.lock b/Gemfile.lock index 349f19061..d554b6f81 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.5) + heroku (3.36.6) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 864f0f64b..318925498 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.5" + VERSION = "3.36.6" end From 085931193d01bca786c7a917149dc0edabf0451a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 5 May 2015 20:21:46 -0700 Subject: [PATCH 532/952] v4 version of heroku status --- lib/heroku/command/status.rb | 33 ++------------------ spec/heroku/command/status_spec.rb | 48 ------------------------------ 2 files changed, 2 insertions(+), 79 deletions(-) delete mode 100644 spec/heroku/command/status_spec.rb diff --git a/lib/heroku/command/status.rb b/lib/heroku/command/status.rb index 0b8f93538..993d6fa5f 100644 --- a/lib/heroku/command/status.rb +++ b/lib/heroku/command/status.rb @@ -16,36 +16,7 @@ class Heroku::Command::Status < Heroku::Command::Base # Production: No known issues at this time. # def index - validate_arguments! - - heroku_status_host = ENV['HEROKU_STATUS_HOST'] || "status.heroku.com" - require('excon') - status = json_decode(Excon.get("https://#{heroku_status_host}/api/v3/current-status.json", :nonblock => false).body) - - styled_header("Heroku Status") - - status['status'].each do |key, value| - if value == 'green' - status['status'][key] = 'No known issues at this time.' - end - end - styled_hash(status['status']) - - unless status['issues'].empty? - display - status['issues'].each do |issue| - duration = time_ago(issue['created_at']).gsub(' ago', '+') - styled_header("#{issue['title']} #{duration}") - changes = issue['updates'].map do |issue| - [ - time_ago(issue['created_at']), - issue['update_type'], - issue['contents'] - ] - end - styled_array(changes, :sort => false) - end - end + Heroku::JSPlugin.install('heroku-status') + Heroku::JSPlugin.run('status', nil, ARGV[1..-1]) end - end diff --git a/spec/heroku/command/status_spec.rb b/spec/heroku/command/status_spec.rb deleted file mode 100644 index d3d02b0d2..000000000 --- a/spec/heroku/command/status_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "spec_helper" -require "heroku/command/status" - -module Heroku::Command - describe Status do - - before(:each) do - stub_core - end - - it "displays status information" do - Excon.stub( - { - :host => 'status.heroku.com', - :method => :get, - :path => '/api/v3/current-status.json' - }, - { - :body => MultiJson.dump({"status"=>{"Production"=>"red", "Development"=>"red"}, "issues"=>[{"created_at"=>"2011-06-07T15:55:51Z", "id"=>372, "resolved"=>false, "title"=>"HTTP Routing Errors", "updated_at"=>"2012-06-07T16:14:37Z", "href"=>"https://status.heroku.com/api/v3/issues/372", "updates"=>[{"contents"=>"The number of applications seeing H99 errors is continuing to decrease as we continue to work toward a full resolution of the HTTP routing issues. The API is back online now as well. ", "created_at"=>"2012-06-07T17:47:26Z", "id"=>1088, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:47:26Z"}, {"contents"=>"Our engineers are continuing to work toward a full resolution of the HTTP routing issues. The API is currently in maintenance mode intentionally as we restore application operations. ", "created_at"=>"2012-06-07T17:16:40Z", "id"=>1086, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T17:26:55Z"}, {"contents"=>"Most applications are back online at this time. Our engineers are working on getting the remaining apps back online. ", "created_at"=>"2012-06-07T16:50:21Z", "id"=>1085, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:50:21Z"}, {"contents"=>"Our routing engineers have pushed out a patch to our routing tier. The platform is recovering and applications are coming back online. Our engineers are continuing to fully restore service.", "created_at"=>"2012-06-07T16:36:37Z", "id"=>1084, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:36:37Z"}, {"contents"=>"We have identified an issue with our routers that is causing errors on HTTP requests to applications. Engineers are working to resolve the issue.\r\n", "created_at"=>"2012-06-07T16:15:25Z", "id"=>1083, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T16:15:28Z"}, {"contents"=>"We have confirmed widespread errors on the platform. Our engineers are continuing to investigate.\r\n", "created_at"=>"2012-06-07T15:58:56Z", "id"=>1082, "incident_id"=>372, "status_dev"=>"red", "status_prod"=>"red", "update_type"=>"update", "updated_at"=>"2012-06-07T15:58:58Z"}, {"contents"=>"Our automated systems have detected potential platform errors. We are investigating.\r\n", "created_at"=>"2012-06-07T15:55:51Z", "id"=>1081, "incident_id"=>372, "status_dev"=>"yellow", "status_prod"=>"yellow", "update_type"=>"issue", "updated_at"=>"2012-06-07T15:55:55Z"}]}]}), - :status => 200 - } - ) - - expect_any_instance_of(Heroku::Command::Status).to receive(:time_ago).and_return('2012/09/11 09:34:56 (~ 3h ago)', '2012/09/11 12:33:56 (~ 1m ago)', '2012/09/11 12:29:56 (~ 5m ago)', '2012/09/11 12:24:56 (~ 10m ago)', '2012/09/11 12:04:56 (~ 30m ago)', '2012/09/11 11:34:56 (~ 1h ago)', '2012/09/11 10:34:56 (~ 2h ago)', '2012/09/11 09:34:56 (~ 3h ago)') - - stderr, stdout = execute("status") - expect(stderr).to eq('') - expect(stdout).to eq <<-STDOUT -=== Heroku Status -Development: red -Production: red - -=== HTTP Routing Errors 2012/09/11 09:34:56 (~ 3h+) -2012/09/11 12:33:56 (~ 1m ago) update The number of applications seeing H99 errors is continuing to decrease as we continue to work toward a full resolution of the HTTP routing issues. The API is back online now as well. -2012/09/11 12:29:56 (~ 5m ago) update Our engineers are continuing to work toward a full resolution of the HTTP routing issues. The API is currently in maintenance mode intentionally as we restore application operations. -2012/09/11 12:24:56 (~ 10m ago) update Most applications are back online at this time. Our engineers are working on getting the remaining apps back online. -2012/09/11 12:04:56 (~ 30m ago) update Our routing engineers have pushed out a patch to our routing tier. The platform is recovering and applications are coming back online. Our engineers are continuing to fully restore service. -2012/09/11 11:34:56 (~ 1h ago) update We have identified an issue with our routers that is causing errors on HTTP requests to applications. Engineers are working to resolve the issue. -2012/09/11 10:34:56 (~ 2h ago) update We have confirmed widespread errors on the platform. Our engineers are continuing to investigate. -2012/09/11 09:34:56 (~ 3h ago) issue Our automated systems have detected potential platform errors. We are investigating. - -STDOUT - - Excon.stubs.shift - end - - end -end From 50575a6cf2f742e14a2d07f2d51ef657a73d77ba Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 13 May 2015 13:27:05 +1000 Subject: [PATCH 533/952] Allow add-on resolver to take an optional arbitrary filter --- lib/heroku/helpers/addons/resolve.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/heroku/helpers/addons/resolve.rb b/lib/heroku/helpers/addons/resolve.rb index 3d3bead6a..673f6977a 100644 --- a/lib/heroku/helpers/addons/resolve.rb +++ b/lib/heroku/helpers/addons/resolve.rb @@ -16,8 +16,8 @@ class AddonDoesNotExistError < Heroku::API::Errors::Error # Finds attachments that match provided identifier. # # Always returns an Array of 0 or more results. - def resolve_attachment(identifier) - case identifier + def resolve_attachment(identifier, &filter) + results = case identifier when UUID [get_attachment(identifier)].compact when ATTACHMENT @@ -35,13 +35,15 @@ def resolve_attachment(identifier) else [] end + + filter ? results.select(&filter) : results end # Finds a single attachment unambiguously given an identifier. # # Returns an attachment hash or exits with an error. - def resolve_attachment!(identifier) - results = resolve_attachment(identifier) + def resolve_attachment!(identifier, &filter) + results = resolve_attachment(identifier, &filter) case results.count when 1 @@ -68,8 +70,8 @@ def resolve_attachment!(identifier) # Returns an array in every case except for when using a service name for an # non-existent add-on. In that case, the error message is returned. # - def resolve_addon(identifier) - case identifier + def resolve_addon(identifier, &filter) + results = case identifier when UUID return [get_addon(identifier)].compact when ATTACHMENT @@ -110,13 +112,15 @@ def resolve_addon(identifier) [] end + + filter ? results.select(&filter) : results end # Finds a single add-on unambiguously given an identifier. # # Returns an add-on hash or exits with an error. - def resolve_addon!(identifier) - results = resolve_addon(identifier) + def resolve_addon!(identifier, &filter) + results = resolve_addon(identifier, &filter) case results.count when 1 From 9eddfcf7115ff13d7f4d36fbeff4aff08ab652d2 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 13 May 2015 13:27:08 +1000 Subject: [PATCH 534/952] Only consider HPG add-ons for pg:promote --- lib/heroku/command/pg.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 1e5846c76..9ec9a2b6d 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -86,7 +86,7 @@ def promote validate_arguments! db = db.sub(/_URL$/, '') # allow promoting with a var name - addon = resolve_addon!(db) + addon = resolve_addon!(db) { |addon| addon['addon_service']['name'] == 'heroku-postgresql' } promoted_name = 'DATABASE' From c6c6311da34248d6710cb63be31aa4d9005f2ef3 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 13 May 2015 13:37:04 +1000 Subject: [PATCH 535/952] Fix specs --- spec/heroku/command/pg_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 3fc7d44a8..23009b882 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -157,7 +157,7 @@ module Heroku::Command before do resource = build_addon( name: "walking-slowly-42", - addon_service: { name: "heroku-posgresql:ronin" }, + addon_service: { name: "heroku-postgresql" }, plan: { name: "ronin" }, app: { id: 1, name: "example" }) @@ -195,7 +195,7 @@ module Heroku::Command it "promotes the specified database resource name" do stderr, stdout = execute("pg:promote walking-slowly-42 --confirm example") expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + expect(stdout).to include <<-STDOUT Promoting walking-slowly-42 to DATABASE_URL on example... done STDOUT expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") @@ -204,7 +204,7 @@ module Heroku::Command it "promotes the specified database by config var" do stderr, stdout = execute("pg:promote HEROKU_POSTGRESQL_RONIN_URL --confirm example") expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + expect(stdout).to include <<-STDOUT Promoting walking-slowly-42 to DATABASE_URL on example... done STDOUT expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") @@ -213,7 +213,7 @@ module Heroku::Command it "promotes the specified database by attachment substring" do stderr, stdout = execute("pg:promote RONIN --confirm example") expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + expect(stdout).to include <<-STDOUT Promoting walking-slowly-42 to DATABASE_URL on example... done STDOUT expect(api.get_config_vars("example").body["DATABASE_URL"]).to eq("postgres://database_url") From 5d5faf4bb8ebc33c2115451d6e5b590cd402038d Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 13 May 2015 14:29:27 +1000 Subject: [PATCH 536/952] Fix displaying attachments by resource when resolving from attachment name --- lib/heroku/helpers/addons/display.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers/addons/display.rb b/lib/heroku/helpers/addons/display.rb index bdb6c1144..c67e7fb23 100644 --- a/lib/heroku/helpers/addons/display.rb +++ b/lib/heroku/helpers/addons/display.rb @@ -35,7 +35,7 @@ def show_for_resource(identifier) display("") # separate sections styled_header("Attachments") - display_attachments(get_attachments(:resource => identifier), ['App', 'Name']) + display_attachments(get_attachments(:resource => resource['id']), ['App', 'Name']) end # Shows all add-ons owned by and attachments attached to the provided app. For example: From 46b95e254d453330e4d4a86ad99e3b5a3d35303f Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 13 May 2015 18:28:29 +1000 Subject: [PATCH 537/952] Don't hide DATABASE_URL from pg:info if only attachment This is a pretty lazy way of solving this problem, but I don't know the consequence of larger changes here. Ideally, this `pg:info` output would be resource-oriented instead of attachment-oriented. --- lib/heroku/command/pg.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 4fce2e24c..97ec4b9f4 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -529,7 +529,8 @@ def hpg_databases_with_info @resolver = generate_resolver dbs = @resolver.all_databases - unique_dbs = dbs.reject { |config, att| 'DATABASE_URL' == config }.map{|config, att| att}.compact + has_promoted = dbs.any? { |_, att| att.primary_attachment? } + unique_dbs = dbs.reject { |var, _| has_promoted && 'DATABASE_URL' == var }.map{|_, att| att}.compact db_infos = {} mutex = Mutex.new From 470626307c33979b32c84106534302acaca70729 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 13 May 2015 18:31:27 +1000 Subject: [PATCH 538/952] Don't resolve DATABASE_URL if already resolved DATABASE_URL is sometimes provided by a real attachment now. --- lib/heroku/helpers/heroku_postgresql.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index 8cd1b1fd9..40cd1ed63 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -108,7 +108,8 @@ def hpg_databases } @hpg_databases = Hash[ pairs ] - if find_database_url_real_attachment + # TODO: don't bother doing this if DATABASE_URL is already present in hash! + if !@hpg_databases.key?('DATABASE_URL') && find_database_url_real_attachment @hpg_databases['DATABASE_URL'] = find_database_url_real_attachment end From 6d65318495430651d8392c98f847e6044bf6c364 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 May 2015 09:28:49 -0700 Subject: [PATCH 539/952] attempt to update plugin on failure --- lib/heroku/plugin.rb | 11 +++++++++-- spec/heroku/plugin_spec.rb | 14 -------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index 3ca004c9c..ad1a7214d 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -62,6 +62,13 @@ def self.load_plugin(plugin) load "#{folder}/init.rb" if File.exists? "#{folder}/init.rb" rescue ScriptError, StandardError => error styled_error(error, "Unable to load plugin #{plugin}.") + action("Updating #{plugin}") do + begin + Heroku::Plugin.new(plugin).update + rescue => e + $stderr.puts(format_with_bang(e.to_s)) + end + end false end end @@ -126,10 +133,10 @@ def update unless git('config --get branch.master.remote').empty? message = git("pull") unless $?.success? - error("Unable to update #{name}.\n" + message) + raise "Unable to update #{name}.\n" + message end else - error(<<-ERROR) + raise <<-ERROR #{name} is a legacy plugin installation. Enable updating by reinstalling with `heroku plugins:install`. ERROR diff --git a/spec/heroku/plugin_spec.rb b/spec/heroku/plugin_spec.rb index 550cf30b2..599161fe1 100644 --- a/spec/heroku/plugin_spec.rb +++ b/spec/heroku/plugin_spec.rb @@ -68,20 +68,6 @@ module Heroku expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("updated\n") end - it "warns on legacy plugins" do - `cd #{@sandbox}/heroku_plugin && git config --unset branch.master.remote` - stderr = capture_stderr do - begin - Plugin.new('heroku_plugin').update - rescue SystemExit - end - end - expect(stderr).to eq <<-STDERR - ! heroku_plugin is a legacy plugin installation. - ! Enable updating by reinstalling with `heroku plugins:install`. -STDERR - end - it "raises exception on symlinked plugins" do `cd #{@sandbox} && ln -s heroku_plugin heroku_plugin_symlink` expect { Plugin.new('heroku_plugin_symlink').update }.to raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin From cbc2a089b43e0834b05e31eb88fb2143ed0437de Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 May 2015 11:50:49 -0700 Subject: [PATCH 540/952] v3.36.7 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 547f19bf1..c767c8f49 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.36.7 2015-05-12 +================= +Update plugins if they fail to load +Show message for ps validation +Moved heroku status to v4 CLI + 3.36.6 2015-05-12 ================= Fixed bug with pgbackups polling diff --git a/Gemfile.lock b/Gemfile.lock index d554b6f81..d10971c4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.6) + heroku (3.36.7) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 318925498..af52c577e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.6" + VERSION = "3.36.7" end From 1073135bf865dde2098fba80ab075bf44468ad56 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 May 2015 11:16:32 -0700 Subject: [PATCH 541/952] make it clear that the CLI is the thing updating --- lib/heroku/updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 9e96c59d4..8a23149c9 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -108,7 +108,7 @@ def self.warn_if_out_of_date def self.update(prerelease=false) return unless prerelease || needs_update? - stderr_print 'updating...' + stderr_print 'updating Heroku CLI...' wait_for_lock do require "tmpdir" require "zip" From 5bf496690019e69be7b37743ff15646cb0dd0e08 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 May 2015 15:38:22 -0700 Subject: [PATCH 542/952] added updated fork help text --- lib/heroku/command/fork.rb | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb index 889961bfd..11f209312 100644 --- a/lib/heroku/command/fork.rb +++ b/lib/heroku/command/fork.rb @@ -6,15 +6,34 @@ module Heroku::Command # class Fork < Base - # fork [NEWNAME] + # fork # - # Fork an existing app -- copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. + # --from FROM # app to fork from + # --to TO # app to create + # -s, --stack STACK # specify a stack for the new app + # --region REGION # specify a region + # --skip-pg # skip postgres databases + # + # Copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. # New app name should not be an existing app. The new app will be created as part of the forking process. # - # -s, --stack STACK # specify a stack for the new app - # --region REGION # specify a region - # --skip-pg # skip postgres databases + #Example: # + # $ heroku fork --from my-production-app --to my-development-app + # Forking my-production-app... done. Forked to my-development-app + # Deploying 60a8b0f to my-development-app... done + # Adding addon memcachier:dev to my-development-app... done + # Adding addon heroku-postgresql:hobby-dev to my-development-app... done + # Transferring HEROKU_POSTGRESQL_AMBER to DATABASE... + # Progress: done + # Copying config vars: + # LANG + # RAILS_ENV + # RACK_ENV + # SECRET_KEY_BASE + # RAILS_SERVE_STATIC_FILES + # ... done + # Fork complete. View it at https://my-development-app.herokuapp.com/ def index Heroku::JSPlugin.install('heroku-fork') Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) From e72e3d6ec8cccfa77000f5cf28e29659f22449ce Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 May 2015 15:45:30 -0700 Subject: [PATCH 543/952] v3.36.8 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c767c8f49..4292b137a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.36.8 2015-05-14 +================= +Changed fork to use --from and --to instead of --app +Added note that process types must be alphanumeric for ps:scale +Fixed issues with DATABASE_URL +Fixed display of addon attachments when specifying attachment name +Improved updating text + 3.36.7 2015-05-12 ================= Update plugins if they fail to load diff --git a/Gemfile.lock b/Gemfile.lock index d10971c4c..f60823e66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.7) + heroku (3.36.8) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index af52c577e..fb6e5e8e3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.7" + VERSION = "3.36.8" end From 91351c153766d44119623bb27632e3983e3d071b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:19:37 -0700 Subject: [PATCH 544/952] copy ca cert for use in v4 --- lib/heroku/jsplugin.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 1ab592ee2..5f50fd5c0 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -4,7 +4,7 @@ class Heroku::JSPlugin extend Heroku::Helpers def self.setup? - @is_setup ||= File.exists? bin + File.exists? bin end def self.try_takeover(command, args) @@ -109,6 +109,7 @@ def self.bin end def self.setup + copy_ca_cert return if File.exist? bin $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) @@ -125,6 +126,13 @@ def self.setup $stderr.puts " done" end + def self.copy_ca_cert + to = File.join(Heroku::Helpers.home_directory, ".heroku", "cacert.pem") + return if File.exists?(to) + from = File.expand_path("../../../data/cacert.pem", __FILE__) + FileUtils.copy(from, to) + end + def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic debug("running #{cmd} on v4") From 6d018d225915054d0ec85074cfbf84a94ba58c93 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:26:50 -0700 Subject: [PATCH 545/952] set cacert for existing users --- lib/heroku/jsplugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 5f50fd5c0..b4ba7d2d1 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -80,6 +80,7 @@ def self.commands end def self.commands_info + copy_ca_cert rescue nil # TODO: remove this once most of the users have the cacert setup @commands_info ||= json_decode(`"#{bin}" commands --json`) end From f023bcdcbb2c76f1002b723d3431205febffcfb5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:30:53 -0700 Subject: [PATCH 546/952] updated cacert.pem --- data/cacert.pem | 521 ++++++++++++++++++++++++++++-------------------- 1 file changed, 307 insertions(+), 214 deletions(-) diff --git a/data/cacert.pem b/data/cacert.pem index 6b981e8aa..23f4a8bcb 100644 --- a/data/cacert.pem +++ b/data/cacert.pem @@ -1,8 +1,7 @@ -## DOWNLOADED FROM: http://curl.haxx.se/ca/cacert.pem ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla downloaded on: Wed Sep 3 03:12:03 2014 +## Certificate data from Mozilla as of: Wed Apr 22 03:12:04 2015 ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates @@ -14,66 +13,11 @@ ## an Apache+mod_ssl webserver for SSL client authentication. ## Just configure this file as the SSLCACertificateFile. ## -## Conversion done with mk-ca-bundle.pl verison 1.22. -## SHA1: c4540021427a6fa29e5f50db9f12d48c97d33889 +## Conversion done with mk-ca-bundle.pl version 1.25. +## SHA1: ed3c0bbfb7912bcc00cd2033b0cb85c98d10559c ## -GTE CyberTrust Global Root -========================== ------BEGIN CERTIFICATE----- -MIICWjCCAcMCAgGlMA0GCSqGSIb3DQEBBAUAMHUxCzAJBgNVBAYTAlVTMRgwFgYDVQQKEw9HVEUg -Q29ycG9yYXRpb24xJzAlBgNVBAsTHkdURSBDeWJlclRydXN0IFNvbHV0aW9ucywgSW5jLjEjMCEG -A1UEAxMaR1RFIEN5YmVyVHJ1c3QgR2xvYmFsIFJvb3QwHhcNOTgwODEzMDAyOTAwWhcNMTgwODEz -MjM1OTAwWjB1MQswCQYDVQQGEwJVUzEYMBYGA1UEChMPR1RFIENvcnBvcmF0aW9uMScwJQYDVQQL -Ex5HVEUgQ3liZXJUcnVzdCBTb2x1dGlvbnMsIEluYy4xIzAhBgNVBAMTGkdURSBDeWJlclRydXN0 -IEdsb2JhbCBSb290MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCVD6C28FCc6HrHiM3dFw4u -sJTQGz0O9pTAipTHBsiQl8i4ZBp6fmw8U+E3KHNgf7KXUwefU/ltWJTSr41tiGeA5u2ylc9yMcql -HHK6XALnZELn+aks1joNrI1CqiQBOeacPwGFVw1Yh0X404Wqk2kmhXBIgD8SFcd5tB8FLztimQID -AQABMA0GCSqGSIb3DQEBBAUAA4GBAG3rGwnpXtlR22ciYaQqPEh346B8pt5zohQDhT37qw4wxYMW -M4ETCJ57NE7fQMh017l93PR2VX2bY1QY6fDq81yx2YtCHrnAlU66+tXifPVoYb+O7AWXX1uw16OF -NMQkpw0PlZPvy5TYnh+dXIVtx6quTx8itc2VrbqnzPmrC3p/ ------END CERTIFICATE----- - -Thawte Server CA -================ ------BEGIN CERTIFICATE----- -MIIDEzCCAnygAwIBAgIBATANBgkqhkiG9w0BAQQFADCBxDELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs -dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcGA1UE -AxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0ZS5j -b20wHhcNOTYwODAxMDAwMDAwWhcNMjAxMjMxMjM1OTU5WjCBxDELMAkGA1UEBhMCWkExFTATBgNV -BAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29u -c3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEZMBcG -A1UEAxMQVGhhd3RlIFNlcnZlciBDQTEmMCQGCSqGSIb3DQEJARYXc2VydmVyLWNlcnRzQHRoYXd0 -ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBANOkUG7I/1Zr5s9dtuoMaHVHoqrC2oQl -/Kj0R1HahbUgdJSGHg91yekIYfUGbTBuFRkC6VLAYttNmZ7iagxEOM3+vuNkCXDF/rFrKbYvScg7 -1CcEJRCXL+eQbcAoQpnXTEPew/UhbVSfXcNY4cDk2VuwuNy0e982OsK1ZiIS1ocNAgMBAAGjEzAR -MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEEBQADgYEAB/pMaVz7lcxG7oWDTSEwjsrZqG9J -GubaUeNgcGyEYRGhGshIPllDfU+VPaGLtwtimHp1it2ITk6eQNuozDJ0uW8NxuOzRAvZim+aKZuZ -GCg70eNAKJpaPNW15yAbi8qkq43pUdniTCxZqdq5snUb9kLy78fyGPmJvKP/iiMucEc= ------END CERTIFICATE----- - -Thawte Premium Server CA -======================== ------BEGIN CERTIFICATE----- -MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkExFTATBgNVBAgT -DFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYDVQQKExRUaGF3dGUgQ29uc3Vs -dGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UE -AxMYVGhhd3RlIFByZW1pdW0gU2VydmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZl -ckB0aGF3dGUuY29tMB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYT -AlpBMRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsGA1UEChMU -VGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRpb24gU2VydmljZXMgRGl2 -aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNlcnZlciBDQTEoMCYGCSqGSIb3DQEJARYZ -cHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2 -aovXwlue2oFBYo847kkEVdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIh -Udib0GfQug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMRuHM/ -qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQQFAAOBgQAm -SCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUIhfzJATj/Tb7yFkJD57taRvvBxhEf -8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JMpAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7t -UCemDaYj+bvLpgcUQg== ------END CERTIFICATE----- - Equifax Secure CA ================= -----BEGIN CERTIFICATE----- @@ -94,25 +38,6 @@ BIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee95 70+sB3c4 -----END CERTIFICATE----- -Verisign Class 3 Public Primary Certification Authority - G2 -============================================================ ------BEGIN CERTIFICATE----- -MIIDAjCCAmsCEH3Z/gfPqB63EHln+6eJNMYwDQYJKoZIhvcNAQEFBQAwgcExCzAJBgNVBAYTAlVT -MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMB4XDTk4MDUxODAwMDAwMFoXDTI4MDgwMTIzNTk1OVowgcExCzAJBgNVBAYTAlVT -MRcwFQYDVQQKEw5WZXJpU2lnbiwgSW5jLjE8MDoGA1UECxMzQ2xhc3MgMyBQdWJsaWMgUHJpbWFy -eSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEcyMTowOAYDVQQLEzEoYykgMTk5OCBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MR8wHQYDVQQLExZWZXJpU2lnbiBUcnVz -dCBOZXR3b3JrMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDMXtERXVxp0KvTuWpMmR9ZmDCO -FoUgRm1HP9SFIIThbbP4pO0M8RcPO/mn+SXXwc+EY/J8Y8+iR/LGWzOOZEAEaMGAuWQcRXfH2G71 -lSk8UOg013gfqLptQ5GVj0VXXn7F+8qkBOvqlzdUMG+7AUcyM83cV5tkaWH4mx0ciU9cZwIDAQAB -MA0GCSqGSIb3DQEBBQUAA4GBAFFNzb5cy5gZnBWyATl4Lk0PZ3BwmcYQWpSkU01UbSuvDV1Ai2TT -1+7eVmGSX6bEHRBhNtMsJzzoKQm5EWR0zLVznxxIqbxhAe7iF6YM40AIOw7n60RzKprxaZLvcRTD -Oaxxp5EJb+RxBrO6WVcmeQD2+A2iMzAo1KpYoJ2daZH9 ------END CERTIFICATE----- - GlobalSign Root CA ================== -----BEGIN CERTIFICATE----- @@ -249,40 +174,6 @@ Y71k5h+3zvDyny67G7fyUIhzksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9H RCwBXbsdtTLSR9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- -Equifax Secure Global eBusiness CA -================================== ------BEGIN CERTIFICATE----- -MIICkDCCAfmgAwIBAgIBATANBgkqhkiG9w0BAQQFADBaMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -RXF1aWZheCBTZWN1cmUgSW5jLjEtMCsGA1UEAxMkRXF1aWZheCBTZWN1cmUgR2xvYmFsIGVCdXNp -bmVzcyBDQS0xMB4XDTk5MDYyMTA0MDAwMFoXDTIwMDYyMTA0MDAwMFowWjELMAkGA1UEBhMCVVMx -HDAaBgNVBAoTE0VxdWlmYXggU2VjdXJlIEluYy4xLTArBgNVBAMTJEVxdWlmYXggU2VjdXJlIEds -b2JhbCBlQnVzaW5lc3MgQ0EtMTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuucXkAJlsTRV -PEnCUdXfp9E3j9HngXNBUmCbnaEXJnitx7HoJpQytd4zjTov2/KaelpzmKNc6fuKcxtc58O/gGzN -qfTWK8D3+ZmqY6KxRwIP1ORROhI8bIpaVIRw28HFkM9yRcuoWcDNM50/o5brhTMhHD4ePmBudpxn -hcXIw2ECAwEAAaNmMGQwEQYJYIZIAYb4QgEBBAQDAgAHMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j -BBgwFoAUvqigdHJQa0S3ySPY+6j/s1draGwwHQYDVR0OBBYEFL6ooHRyUGtEt8kj2Puo/7NXa2hs -MA0GCSqGSIb3DQEBBAUAA4GBADDiAVGqx+pf2rnQZQ8w1j7aDRRJbpGTJxQx78T3LUX47Me/okEN -I7SS+RkAZ70Br83gcfxaz2TE4JaY0KNA4gGK7ycH8WUBikQtBmV1UsCGECAhX2xrD2yuCRyv8qIY -NMR1pHMc8Y3c7635s3a0kr/clRAevsvIO1qEYBlWlKlV ------END CERTIFICATE----- - -Equifax Secure eBusiness CA 1 -============================= ------BEGIN CERTIFICATE----- -MIICgjCCAeugAwIBAgIBBDANBgkqhkiG9w0BAQQFADBTMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -RXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNzIENB -LTEwHhcNOTkwNjIxMDQwMDAwWhcNMjAwNjIxMDQwMDAwWjBTMQswCQYDVQQGEwJVUzEcMBoGA1UE -ChMTRXF1aWZheCBTZWN1cmUgSW5jLjEmMCQGA1UEAxMdRXF1aWZheCBTZWN1cmUgZUJ1c2luZXNz -IENBLTEwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM4vGbwXt3fek6lfWg0XTzQaDJj0ItlZ -1MRoRvC0NcWFAyDGr0WlIVFFQesWWDYyb+JQYmT5/VGcqiTZ9J2DKocKIdMSODRsjQBuWqDZQu4a -IZX5UkxVWsUPOE9G+m34LjXWHXzr4vCwdYDIqROsvojvOm6rXyo4YgKwEnv+j6YDAgMBAAGjZjBk -MBEGCWCGSAGG+EIBAQQEAwIABzAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEp4MlIR21kW -Nl7fwRQ2QGpHfEyhMB0GA1UdDgQWBBRKeDJSEdtZFjZe38EUNkBqR3xMoTANBgkqhkiG9w0BAQQF -AAOBgQB1W6ibAxHm6VZMzfmpTMANmvPMZWnmJXbMWbfWVMMdzZmsGd20hdXgPfxiIKeES1hl8eL5 -lSE/9dR+WB5Hh1Q+WKG1tfgq73HnvMP2sUlG4tega+VWeponmHxGYhTnyfxuAxJ5gDgdSIKN/Bf+ -KpYrtWKmpj29f5JZzVoqgrI3eQ== ------END CERTIFICATE----- - AddTrust Low-Value Services Root ================================ -----BEGIN CERTIFICATE----- @@ -528,59 +419,6 @@ gn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwXQMAJKOSLakhT2+zNVVXxxvjpoixMptEm X36vWkzaH6byHCx+rgIW0lbQL1dTR+iS -----END CERTIFICATE----- -America Online Root Certification Authority 1 -============================================= ------BEGIN CERTIFICATE----- -MIIDpDCCAoygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSAxMB4XDTAyMDUyODA2MDAwMFoXDTM3MTExOTIwNDMwMFowYzELMAkG -A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg -T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAKgv6KRpBgNHw+kqmP8ZonCaxlCyfqXfaE0bfA+2l2h9LaaLl+lkhsmj76CG -v2BlnEtUiMJIxUo5vxTjWVXlGbR0yLQFOVwWpeKVBeASrlmLojNoWBym1BW32J/X3HGrfpq/m44z -DyL9Hy7nBzbvYjnF3cu6JRQj3gzGPTzOggjmZj7aUTsWOqMFf6Dch9Wc/HKpoH145LcxVR5lu9Rh -sCFg7RAycsWSJR74kEoYeEfffjA3PlAb2xzTa5qGUwew76wGePiEmf4hjUyAtgyC9mZweRrTT6PP -8c9GsEsPPt2IYriMqQkoO3rHl+Ee5fSfwMCuJKDIodkP1nsmgmkyPacCAwEAAaNjMGEwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUAK3Zo/Z59m50qX8zPYEX10zPM94wHwYDVR0jBBgwFoAUAK3Z -o/Z59m50qX8zPYEX10zPM94wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBBQUAA4IBAQB8itEf -GDeC4Liwo+1WlchiYZwFos3CYiZhzRAW18y0ZTTQEYqtqKkFZu90821fnZmv9ov761KyBZiibyrF -VL0lvV+uyIbqRizBs73B6UlwGBaXCBOMIOAbLjpHyx7kADCVW/RFo8AasAFOq73AI25jP4BKxQft -3OJvx8Fi8eNy1gTIdGcL+oiroQHIb/AUr9KZzVGTfu0uOMe9zkZQPXLjeSWdm4grECDdpbgyn43g -Kd8hdIaC2y+CMMbHNYaz+ZZfRtsMRf3zUMNvxsNIrUam4SdHCh0Om7bCd39j8uB9Gr784N/Xx6ds -sPmuujz9dLQR6FgNgLzTqIA6me11zEZ7 ------END CERTIFICATE----- - -America Online Root Certification Authority 2 -============================================= ------BEGIN CERTIFICATE----- -MIIFpDCCA4ygAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEcMBoGA1UEChMT -QW1lcmljYSBPbmxpbmUgSW5jLjE2MDQGA1UEAxMtQW1lcmljYSBPbmxpbmUgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyODA2MDAwMFoXDTM3MDkyOTE0MDgwMFowYzELMAkG -A1UEBhMCVVMxHDAaBgNVBAoTE0FtZXJpY2EgT25saW5lIEluYy4xNjA0BgNVBAMTLUFtZXJpY2Eg -T25saW5lIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQAD -ggIPADCCAgoCggIBAMxBRR3pPU0Q9oyxQcngXssNt79Hc9PwVU3dxgz6sWYFas14tNwC206B89en -fHG8dWOgXeMHDEjsJcQDIPT/DjsS/5uN4cbVG7RtIuOx238hZK+GvFciKtZHgVdEglZTvYYUAQv8 -f3SkWq7xuhG1m1hagLQ3eAkzfDJHA1zEpYNI9FdWboE2JxhP7JsowtS013wMPgwr38oE18aO6lhO -qKSlGBxsRZijQdEt0sdtjRnxrXm3gT+9BoInLRBYBbV4Bbkv2wxrkJB+FFk4u5QkE+XRnRTf04JN -RvCAOVIyD+OEsnpD8l7eXz8d3eOyG6ChKiMDbi4BFYdcpnV1x5dhvt6G3NRI270qv0pV2uh9UPu0 -gBe4lL8BPeraunzgWGcXuVjgiIZGZ2ydEEdYMtA1fHkqkKJaEBEjNa0vzORKW6fIJ/KD3l67Xnfn -6KVuY8INXWHQjNJsWiEOyiijzirplcdIz5ZvHZIlyMbGwcEMBawmxNJ10uEqZ8A9W6Wa6897Gqid -FEXlD6CaZd4vKL3Ob5Rmg0gp2OpljK+T2WSfVVcmv2/LNzGZo2C7HK2JNDJiuEMhBnIMoVxtRsX6 -Kc8w3onccVvdtjc+31D1uAclJuW8tf48ArO3+L5DwYcRlJ4jbBeKuIonDFRH8KmzwICMoCfrHRnj -B453cMor9H124HhnAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE1FwWg4u3Op -aaEg5+31IqEjFNeeMB8GA1UdIwQYMBaAFE1FwWg4u3OpaaEg5+31IqEjFNeeMA4GA1UdDwEB/wQE -AwIBhjANBgkqhkiG9w0BAQUFAAOCAgEAZ2sGuV9FOypLM7PmG2tZTiLMubekJcmnxPBUlgtk87FY -T15R/LKXeydlwuXK5w0MJXti4/qftIe3RUavg6WXSIylvfEWK5t2LHo1YGwRgJfMqZJS5ivmae2p -+DYtLHe/YUjRYwu5W1LtGLBDQiKmsXeu3mnFzcccobGlHBD7GL4acN3Bkku+KVqdPzW+5X1R+FXg -JXUjhx5c3LqdsKyzadsXg8n33gy8CNyRnqjQ1xU3c6U1uPx+xURABsPr+CKAXEfOAuMRn0T//Zoy -zH1kUQ7rVyZ2OuMeIjzCpjbdGe+n/BLzJsBZMYVMnNjP36TMzCmT/5RtdlwTCJfy7aULTd3oyWgO -ZtMADjMSW7yV5TKQqLPGbIOtd+6Lfn6xqavT4fG2wLHqiMDn05DpKJKUe2h7lyoKZy2FAjgQ5ANh -1NolNscIWC2hp1GvMApJ9aZphwctREZ2jirlmjvXGKL8nDgQzMY70rUXOm/9riW99XJZZLF0Kjhf -GEzfz3EEWjbUvy+ZnOjZurGV5gJLIaFb1cFPj65pbVPbAZO1XB4Y3WRayhgoPmMEEf0cjQAPuDff -Z4qdZqkCapH/E8ovXYO8h5Ns3CRRFgQlZvqz2cK6Kb6aSDiCmfS/O0oxGfm/jiEzFMpPVF/7zvuP -cX/9XhmgD0uRuMRUvAawRY8mkaKO/qk= ------END CERTIFICATE----- - Visa eCommerce Root =================== -----BEGIN CERTIFICATE----- @@ -1778,33 +1616,6 @@ JOzHdiEoZa5X6AeIdUpWoNIFOqTmjZKILPPy4cHGYdtBxceb9w4aUUXCYWvcZCcXjFq32nQozZfk vQ== -----END CERTIFICATE----- -TC TrustCenter Class 3 CA II -============================ ------BEGIN CERTIFICATE----- -MIIEqjCCA5KgAwIBAgIOSkcAAQAC5aBd1j8AUb8wDQYJKoZIhvcNAQEFBQAwdjELMAkGA1UEBhMC -REUxHDAaBgNVBAoTE1RDIFRydXN0Q2VudGVyIEdtYkgxIjAgBgNVBAsTGVRDIFRydXN0Q2VudGVy -IENsYXNzIDMgQ0ExJTAjBgNVBAMTHFRDIFRydXN0Q2VudGVyIENsYXNzIDMgQ0EgSUkwHhcNMDYw -MTEyMTQ0MTU3WhcNMjUxMjMxMjI1OTU5WjB2MQswCQYDVQQGEwJERTEcMBoGA1UEChMTVEMgVHJ1 -c3RDZW50ZXIgR21iSDEiMCAGA1UECxMZVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQTElMCMGA1UE -AxMcVEMgVHJ1c3RDZW50ZXIgQ2xhc3MgMyBDQSBJSTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBALTgu1G7OVyLBMVMeRwjhjEQY0NVJz/GRcekPewJDRoeIMJWHt4bNwcwIi9v8Qbxq63W -yKthoy9DxLCyLfzDlml7forkzMA5EpBCYMnMNWju2l+QVl/NHE1bWEnrDgFPZPosPIlY2C8u4rBo -6SI7dYnWRBpl8huXJh0obazovVkdKyT21oQDZogkAHhg8fir/gKya/si+zXmFtGt9i4S5Po1auUZ -uV3bOx4a+9P/FRQI2AlqukWdFHlgfa9Aigdzs5OW03Q0jTo3Kd5c7PXuLjHCINy+8U9/I1LZW+Jk -2ZyqBwi1Rb3R0DHBq1SfqdLDYmAD8bs5SpJKPQq5ncWg/jcCAwEAAaOCATQwggEwMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTUovyfs8PYA9NXXAek0CSnwPIA1DCB -7QYDVR0fBIHlMIHiMIHfoIHcoIHZhjVodHRwOi8vd3d3LnRydXN0Y2VudGVyLmRlL2NybC92Mi90 -Y19jbGFzc18zX2NhX0lJLmNybIaBn2xkYXA6Ly93d3cudHJ1c3RjZW50ZXIuZGUvQ049VEMlMjBU -cnVzdENlbnRlciUyMENsYXNzJTIwMyUyMENBJTIwSUksTz1UQyUyMFRydXN0Q2VudGVyJTIwR21i -SCxPVT1yb290Y2VydHMsREM9dHJ1c3RjZW50ZXIsREM9ZGU/Y2VydGlmaWNhdGVSZXZvY2F0aW9u -TGlzdD9iYXNlPzANBgkqhkiG9w0BAQUFAAOCAQEANmDkcPcGIEPZIxpC8vijsrlNirTzwppVMXzE -O2eatN9NDoqTSheLG43KieHPOh6sHfGcMrSOWXaiQYUlN6AT0PV8TtXqluJucsG7Kv5sbviRmEb8 -yRtXW+rIGjs/sFGYPAfaLFkB2otE6OF0/ado3VS6g0bsyEa1+K+XwDsJHI/OcpY9M1ZwvJbL2NV9 -IJqDnxrcOfHFcqMRA/07QlIp2+gB95tejNaNhk4Z+rwcvsUhpYeeeC422wlxo3I0+GzjBgnyXlal -092Y+tTmBvTwtiBjS+opvaqCZh77gaqnN60TGOaSw4HBM7uIHqHn4rS9MWwOUT1v+5ZWgOI2F9Hc -5A== ------END CERTIFICATE----- - TC TrustCenter Universal CA I ============================= -----BEGIN CERTIFICATE----- @@ -2422,28 +2233,6 @@ yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi LXpUq3DDfSJlgnCW -----END CERTIFICATE----- -E-Guven Kok Elektronik Sertifika Hizmet Saglayicisi -=================================================== ------BEGIN CERTIFICATE----- -MIIDtjCCAp6gAwIBAgIQRJmNPMADJ72cdpW56tustTANBgkqhkiG9w0BAQUFADB1MQswCQYDVQQG -EwJUUjEoMCYGA1UEChMfRWxla3Ryb25payBCaWxnaSBHdXZlbmxpZ2kgQS5TLjE8MDoGA1UEAxMz -ZS1HdXZlbiBLb2sgRWxla3Ryb25payBTZXJ0aWZpa2EgSGl6bWV0IFNhZ2xheWljaXNpMB4XDTA3 -MDEwNDExMzI0OFoXDTE3MDEwNDExMzI0OFowdTELMAkGA1UEBhMCVFIxKDAmBgNVBAoTH0VsZWt0 -cm9uaWsgQmlsZ2kgR3V2ZW5saWdpIEEuUy4xPDA6BgNVBAMTM2UtR3V2ZW4gS29rIEVsZWt0cm9u -aWsgU2VydGlmaWthIEhpem1ldCBTYWdsYXlpY2lzaTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC -AQoCggEBAMMSIJ6wXgBljU5Gu4Bc6SwGl9XzcslwuedLZYDBS75+PNdUMZTe1RK6UxYC6lhj71vY -8+0qGqpxSKPcEC1fX+tcS5yWCEIlKBHMilpiAVDV6wlTL/jDj/6z/P2douNffb7tC+Bg62nsM+3Y -jfsSSYMAyYuXjDtzKjKzEve5TfL0TW3H5tYmNwjy2f1rXKPlSFxYvEK+A1qBuhw1DADT9SN+cTAI -JjjcJRFHLfO6IxClv7wC90Nex/6wN1CZew+TzuZDLMN+DfIcQ2Zgy2ExR4ejT669VmxMvLz4Bcpk -9Ok0oSy1c+HCPujIyTQlCFzz7abHlJ+tiEMl1+E5YP6sOVkCAwEAAaNCMEAwDgYDVR0PAQH/BAQD -AgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJ/uRLOU1fqRTy7ZVZoEVtstxNulMA0GCSqG -SIb3DQEBBQUAA4IBAQB/X7lTW2M9dTLn+sR0GstG30ZpHFLPqk/CaOv/gKlR6D1id4k9CnU58W5d -F4dvaAXBlGzZXd/aslnLpRCKysw5zZ/rTt5S/wzw9JKp8mxTq5vSR6AfdPebmvEvFZ96ZDAYBzwq -D2fK/A+JYZ1lpTzlvBNbCNvj/+27BrtqBrF6T2XGgv0enIu1De5Iu7i9qgi0+6N8y5/NkHZchpZ4 -Vwpm+Vganf2XKWDeEaaQHBkc7gGWIjQ0LpH5t8Qn0Xvmv/uARFoW5evg1Ao4vOSR49XrXMGs3xtq -fJ7lddK2l4fbzIcrQzqECK+rPNv3PGYxhrCdU3nt+CPeQuMtgvEP5fqX ------END CERTIFICATE----- - GlobalSign Root CA - R3 ======================= -----BEGIN CERTIFICATE----- @@ -3893,3 +3682,307 @@ ONFLAzkopR6RctR9q5czxNM+4Gm2KHmgCY0c0f9BckgG/Jou5yD5m6Leie2uPAmvylezkolwQOQv T8Jwg0DXJCxr5wkf09XHwQj02w47HAcLQxGEIYbpgNR12KvxAmLBsX5VYc8T1yaw15zLKYs4SgsO kI26oQ== -----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +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 +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +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 +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB4TCCAYegAwIBAgIRKjikHJYKBN5CsiilC+g0mAIwCgYIKoZIzj0EAwIwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI0MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuMZ5049sJQ6fLjkZHAOkrprl +OQcJFspjsbmG+IpXwVfOQvpzofdlQv8ewQCybnMO/8ch5RikqtlxP6jUuc6MHaNCMEAwDgYDVR0P +AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFFSwe61FuOJAf/sKbvu+M8k8o4TV +MAoGCCqGSM49BAMCA0gAMEUCIQDckqGgE6bPA7DmxCGXkPoUVy0D7O48027KqGx2vKLeuwIgJ6iF +JzWbVsaj8kfSt24bAgAXqmemFZHe+pTsewv4n4Q= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +Staat der Nederlanden Root CA - G3 +================================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIEAJiiOTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +Um9vdCBDQSAtIEczMB4XDTEzMTExNDExMjg0MloXDTI4MTExMzIzMDAwMFowWjELMAkGA1UEBhMC +TkwxHjAcBgNVBAoMFVN0YWF0IGRlciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5l +ZGVybGFuZGVuIFJvb3QgQ0EgLSBHMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL4y +olQPcPssXFnrbMSkUeiFKrPMSjTysF/zDsccPVMeiAho2G89rcKezIJnByeHaHE6n3WWIkYFsO2t +x1ueKt6c/DrGlaf1F2cY5y9JCAxcz+bMNO14+1Cx3Gsy8KL+tjzk7FqXxz8ecAgwoNzFs21v0IJy +EavSgWhZghe3eJJg+szeP4TrjTgzkApyI/o1zCZxMdFyKJLZWyNtZrVtB0LrpjPOktvA9mxjeM3K +Tj215VKb8b475lRgsGYeCasH/lSJEULR9yS6YHgamPfJEf0WwTUaVHXvQ9Plrk7O53vDxk5hUUur +mkVLoR9BvUhTFXFkC4az5S6+zqQbwSmEorXLCCN2QyIkHxcE1G6cxvx/K2Ya7Irl1s9N9WMJtxU5 +1nus6+N86U78dULI7ViVDAZCopz35HCz33JvWjdAidiFpNfxC95DGdRKWCyMijmev4SH8RY7Ngzp +07TKbBlBUgmhHbBqv4LvcFEhMtwFdozL92TkA1CvjJFnq8Xy7ljY3r735zHPbMk7ccHViLVlvMDo +FxcHErVc0qsgk7TmgoNwNsXNo42ti+yjwUOH5kPiNL6VizXtBznaqB16nzaeErAMZRKQFWDZJkBE +41ZgpRDUajz9QdwOWke275dhdU/Z/seyHdTtXUmzqWrLZoQT1Vyg3N9udwbRcXXIV2+vD3dbAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRUrfrHkleu +yjWcLhL75LpdINyUVzANBgkqhkiG9w0BAQsFAAOCAgEAMJmdBTLIXg47mAE6iqTnB/d6+Oea31BD +U5cqPco8R5gu4RV78ZLzYdqQJRZlwJ9UXQ4DO1t3ApyEtg2YXzTdO2PCwyiBwpwpLiniyMMB8jPq +KqrMCQj3ZWfGzd/TtiunvczRDnBfuCPRy5FOCvTIeuXZYzbB1N/8Ipf3YF3qKS9Ysr1YvY2WTxB1 +v0h7PVGHoTx0IsL8B3+A3MSs/mrBcDCw6Y5p4ixpgZQJut3+TcCDjJRYwEYgr5wfAvg1VUkvRtTA +8KCWAg8zxXHzniN9lLf9OtMJgwYh/WA9rjLA0u6NpvDntIJ8CsxwyXmA+P5M9zWEGYox+wrZ13+b +8KKaa8MFSu1BYBQw0aoRQm7TIwIEC8Zl3d1Sd9qBa7Ko+gE4uZbqKmxnl4mUnrzhVNXkanjvSr0r +mj1AfsbAddJu+2gw7OyLnflJNZoaLNmzlTnVHpL3prllL+U9bTpITAjc5CgSKL59NVzq4BZ+Extq +1z7XnvwtdbLBFNUjA9tbbws+eC8N3jONFrdI54OagQ97wUNNVQQXOEpR1VmiiXTTn74eS9fGbbeI +JG9gkaSChVtWQbzQRKtqE77RLFi3EjNYsjdj3BP1lB0/QFH1T/U67cjF68IeHRaVesd+QnGTbksV +tzDfqu1XhUisHWrdOWnk4Xl4vs4Fv6EM94B7IWcnMFk= +-----END CERTIFICATE----- + +Staat der Nederlanden EV Root CA +================================ +-----BEGIN CERTIFICATE----- +MIIFcDCCA1igAwIBAgIEAJiWjTANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJOTDEeMBwGA1UE +CgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSkwJwYDVQQDDCBTdGFhdCBkZXIgTmVkZXJsYW5kZW4g +RVYgUm9vdCBDQTAeFw0xMDEyMDgxMTE5MjlaFw0yMjEyMDgxMTEwMjhaMFgxCzAJBgNVBAYTAk5M +MR4wHAYDVQQKDBVTdGFhdCBkZXIgTmVkZXJsYW5kZW4xKTAnBgNVBAMMIFN0YWF0IGRlciBOZWRl +cmxhbmRlbiBFViBSb290IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA48d+ifkk +SzrSM4M1LGns3Amk41GoJSt5uAg94JG6hIXGhaTK5skuU6TJJB79VWZxXSzFYGgEt9nCUiY4iKTW +O0Cmws0/zZiTs1QUWJZV1VD+hq2kY39ch/aO5ieSZxeSAgMs3NZmdO3dZ//BYY1jTw+bbRcwJu+r +0h8QoPnFfxZpgQNH7R5ojXKhTbImxrpsX23Wr9GxE46prfNeaXUmGD5BKyF/7otdBwadQ8QpCiv8 +Kj6GyzyDOvnJDdrFmeK8eEEzduG/L13lpJhQDBXd4Pqcfzho0LKmeqfRMb1+ilgnQ7O6M5HTp5gV +XJrm0w912fxBmJc+qiXbj5IusHsMX/FjqTf5m3VpTCgmJdrV8hJwRVXj33NeN/UhbJCONVrJ0yPr +08C+eKxCKFhmpUZtcALXEPlLVPxdhkqHz3/KRawRWrUgUY0viEeXOcDPusBCAUCZSCELa6fS/ZbV +0b5GnUngC6agIk440ME8MLxwjyx1zNDFjFE7PZQIZCZhfbnDZY8UnCHQqv0XcgOPvZuM5l5Tnrmd +74K74bzickFbIZTTRTeU0d8JOV3nI6qaHcptqAqGhYqCvkIH1vI4gnPah1vlPNOePqc7nvQDs/nx +fRN0Av+7oeX6AHkcpmZBiFxgV6YuCcS6/ZrPpx9Aw7vMWgpVSzs4dlG4Y4uElBbmVvMCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP6rAJCYniT8qcwa +ivsnuL8wbqg7MA0GCSqGSIb3DQEBCwUAA4ICAQDPdyxuVr5Os7aEAJSrR8kN0nbHhp8dB9O2tLsI +eK9p0gtJ3jPFrK3CiAJ9Brc1AsFgyb/E6JTe1NOpEyVa/m6irn0F3H3zbPB+po3u2dfOWBfoqSmu +c0iH55vKbimhZF8ZE/euBhD/UcabTVUlT5OZEAFTdfETzsemQUHSv4ilf0X8rLiltTMMgsT7B/Zq +5SWEXwbKwYY5EdtYzXc7LMJMD16a4/CrPmEbUCTCwPTxGfARKbalGAKb12NMcIxHowNDXLldRqAN +b/9Zjr7dn3LDWyvfjFvO5QxGbJKyCqNMVEIYFRIYvdr8unRu/8G2oGTYqV9Vrp9canaW2HNnh/tN +f1zuacpzEPuKqf2evTY4SUmH9A4U8OmHuD+nT3pajnnUk+S7aFKErGzp85hwVXIy+TSrK0m1zSBi +5Dp6Z2Orltxtrpfs/J92VoguZs9btsmksNcFuuEnL5O7Jiqik7Ab846+HUCjuTaPPoIaGl6I6lD4 +WeKDRikL40Rc4ZW2aZCaFG+XroHPaO+Zmr615+F/+PoTRxZMzG0IQOeLeG9QgkRQP2YGiqtDhFZK +DyAthg710tvSeopLzaXoTvFeJiUBWSOgftL2fiFX1ye8FVdMpEbB4IMeDExNH08GGeL5qPQ6gqGy +eUN51q1veieQA6TqJIc/2b3Z6fJfUEkc7uzXLg== +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +Entrust Root Certification Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy +bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug +b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw +HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT +DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx +OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s +eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP +/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz +HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU +s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y +TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx +AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 +0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z +iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ +Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi +nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ +vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO +e4pIb4tF9g== +-----END CERTIFICATE----- + +Entrust Root Certification Authority - EC1 +========================================== +-----BEGIN CERTIFICATE----- +MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx +FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn +YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl +ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw +FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs +LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg +dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt +IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy +AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef +9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h +vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 +kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- From 6047a2ab2c36bc18991fb8bd90b20f2a4abf2228 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:40:50 -0700 Subject: [PATCH 547/952] v3.36.9 --- CHANGELOG | 6 ++++++ lib/heroku/version.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4292b137a..09cdeb5f2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.36.9 2015-05-15 +================= +Updated CA Certs +Add cacert.pem to ~/.heroku for use in v4 CLI +Fix pgbackups:unschedule issues + 3.36.8 2015-05-14 ================= Changed fork to use --from and --to instead of --app diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index fb6e5e8e3..3b42be010 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.8" + VERSION = "3.36.9" end From bc599bf2b967e1e880faafdbb81d6406c06fc82f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:43:19 -0700 Subject: [PATCH 548/952] gemfile.lock updated --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index f60823e66..b8f18ed0e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.8) + heroku (3.36.9) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) From ab4d1b33a4291425c1187610b99e7b4bc338d2a5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:44:27 -0700 Subject: [PATCH 549/952] v3.36.10 (forgot to update Gemfile.lock in 3.36.9) --- CHANGELOG | 4 ++-- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 09cdeb5f2..dadf4cad6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ -3.36.9 2015-05-15 -================= +3.36.10 2015-05-15 +================== Updated CA Certs Add cacert.pem to ~/.heroku for use in v4 CLI Fix pgbackups:unschedule issues diff --git a/Gemfile.lock b/Gemfile.lock index b8f18ed0e..62d0279ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.9) + heroku (3.36.10) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3b42be010..19dceed6f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.9" + VERSION = "3.36.10" end From 44d45af3732b0f7f72e028ba9a840d4813b3f5fb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:48:14 -0700 Subject: [PATCH 550/952] allow disabling of ssl with env var when setting up v4 --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index b4ba7d2d1..3b578fee2 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -171,7 +171,7 @@ def self.manifest end def self.excon_opts - if os == 'windows' + if os == 'windows' || ENV['HEROKU_SSL_VERIFY'] == 'disable' # S3 SSL downloads do not work from ruby in Windows {:ssl_verify_peer => false} else From 2077fcad2c5445307f1537843b83ded7fac55d87 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 16:49:03 -0700 Subject: [PATCH 551/952] v3.36.11 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dadf4cad6..7de14825d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.36.11 2015-05-15 +================== +Allow HEROKU_SSL_VERIFY=disable for v4 setup + 3.36.10 2015-05-15 ================== Updated CA Certs diff --git a/Gemfile.lock b/Gemfile.lock index 62d0279ef..b863b42b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.10) + heroku (3.36.11) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 19dceed6f..9606b35a1 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.10" + VERSION = "3.36.11" end From b619b2aa96dfbc1b0a1a0395899c040b4dbcacf4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 17:01:43 -0700 Subject: [PATCH 552/952] fixed version check in updater to not use strings --- lib/heroku/updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 8a23149c9..78219e32c 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -133,7 +133,7 @@ def self.update(prerelease=false) version = client_version_from_path(download_dir) # do not replace beta version if it is old - return if version < latest_local_version + return if compare_versions(version, latest_local_version) < 0 FileUtils.rm_rf updated_client_path FileUtils.mkdir_p File.dirname(updated_client_path) From 379b92b3f9c0f32ad2a20fb06255c07e9238ca40 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 17:03:14 -0700 Subject: [PATCH 553/952] v3.37.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7de14825d..0e38bad9d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.37.0 2015-05-16 +================= +Fixed bug in updater checking version strings + 3.36.11 2015-05-15 ================== Allow HEROKU_SSL_VERIFY=disable for v4 setup diff --git a/Gemfile.lock b/Gemfile.lock index b863b42b8..add819903 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.36.11) + heroku (3.37.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9606b35a1..3eaab4a3b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.36.11" + VERSION = "3.37.0" end From a04632c4b65940551f27346d21743aec7805fdb2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 18:05:37 -0700 Subject: [PATCH 554/952] copy cert after creating directory for it --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 3b578fee2..ac1f40280 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -110,10 +110,10 @@ def self.bin end def self.setup - copy_ca_cert return if File.exist? bin $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) + copy_ca_cert opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) resp = Excon.get(url, opts) open(bin, "wb") do |file| From 91493eb2681213297078f06d84a4d7f6a9b002ea Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 15 May 2015 19:19:50 -0700 Subject: [PATCH 555/952] hide hidden topics --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index ac1f40280..b54cb283b 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -25,7 +25,7 @@ def self.load! Heroku::Command.register_namespace( :name => topic['name'], :description => " #{topic['description']}" - ) unless Heroku::Command.namespaces.include?(topic['name']) + ) unless topic['hidden'] || Heroku::Command.namespaces.include?(topic['name']) end commands.each do |plugin| help = "\n\n #{plugin['fullHelp'].split("\n").join("\n ")}" From 26d0aacb848ef94ac93a0a2ab560e121372f0c51 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 18 May 2015 11:24:13 -0700 Subject: [PATCH 556/952] v3.37.1 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0e38bad9d..d3ed028f9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.37.1 2015-05-18 +================= +Hide hidden commands +Fixed bug with copying cacert when `~/.heroku` does not exist + 3.37.0 2015-05-16 ================= Fixed bug in updater checking version strings diff --git a/Gemfile.lock b/Gemfile.lock index add819903..9aa0aa496 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.0) + heroku (3.37.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3eaab4a3b..692961c5c 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.0" + VERSION = "3.37.1" end From e36f991d75c36f3b3269491f987b50c31a0c8be6 Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Mon, 18 May 2015 15:35:22 -0700 Subject: [PATCH 557/952] Display price on addons:create --- lib/heroku/command/addons.rb | 2 ++ spec/heroku/command/addons_spec.rb | 8 ++++---- spec/support/addons_helper.rb | 3 +++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 6a8e38b05..6ded9919e 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -124,12 +124,14 @@ def create # endpoint is designed to communicate this data. # # WARNING: Do not depend on this having any effect permanently. + "Accept-Expansion" => "plan", "X-Heroku-Legacy-Provider-Messages" => "true" }, :expects => 201, :method => :post, :path => "/apps/#{app}/addons" ) + @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') action("Creating #{addon['name'].downcase}") {} action("Adding #{addon['name'].downcase} to #{app}") {} diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 0d4ba0610..7668885f8 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -382,7 +382,7 @@ module Heroku::Command name: "my_addon", addon_service: { name: "my_addon" }, app: { name: "example" } - ).merge(provision_message: "OMG A MESSAGE") + ).merge(provision_message: "OMG A MESSAGE", plan: { price: { 'cents' => 1000, 'unit' => 'month' }}) { body: MultiJson.encode(addon), status: 201 } end @@ -390,7 +390,7 @@ module Heroku::Command stderr, stdout = execute("addons:create my_addon") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Creating my_addon... done +Creating my_addon... done, ($10.00/month) Adding my_addon to example... done OMG A MESSAGE Use `heroku addons:docs my_addon` to view documentation. @@ -412,7 +412,7 @@ module Heroku::Command stderr, stdout = execute("addons:create my_addon:test") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Creating my_addon... done +Creating my_addon... done, (free) Adding my_addon to example... done Use `heroku addons:docs my_addon` to view documentation. OUTPUT @@ -435,7 +435,7 @@ module Heroku::Command stderr, stdout = execute("addons:create my_addon") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Creating my_addon... done +Creating my_addon... done, (free) Adding my_addon to example... done foo bar diff --git a/spec/support/addons_helper.rb b/spec/support/addons_helper.rb index f4ca3f1a7..949ae8309 100644 --- a/spec/support/addons_helper.rb +++ b/spec/support/addons_helper.rb @@ -14,6 +14,9 @@ def build_addon(addon={}) plan: { id: SecureRandom.uuid, + price: { + cents: 0, unit: 'month' + } }.merge(addon.fetch(:plan, {})), app: { From a67a123bf99536ee44a3cd8242e0bc4f4a6a1a09 Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Tue, 19 May 2015 10:54:20 -0700 Subject: [PATCH 558/952] Display price on addons:upgrade/downgrade --- lib/heroku/command/addons.rb | 10 +++++++--- spec/heroku/command/addons_spec.rb | 13 +++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 6ded9919e..a224b46ca 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -256,15 +256,19 @@ def upgrade raise CommandFailed.new("Missing add-on plan") if plan.nil? action("Changing #{addon_name} plan to #{plan}") do - api.request( + addon = api.request( :body => json_encode({ "plan" => { "name" => plan } }), :expects => 200..300, - :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.switzerland", + "Accept-Expansion" => "plan" + }, :method => :patch, :path => "/apps/#{app}/addons/#{addon_name}" - ) + ).body + @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') end end diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 7668885f8..00f9433de 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -489,7 +489,7 @@ module Heroku::Command expect(@addons.api).to receive(:request) { |args| expect(args[:method]).to eq :patch expect(args[:path]).to eq "/apps/example/addons/my_addon" - } + }.and_return(OpenStruct.new(body: stringify(addon))) @addons.upgrade end @@ -507,7 +507,8 @@ module Heroku::Command name: "my_addon", plan: { name: "my_plan" }, addon_service: { name: "my_service" }, - app: { name: "example" }) + app: { name: "example" }, + price: { cents: 0, unit: "month" }) Excon.stub(method: :get, path: %r(/apps/example/addons)) do { body: MultiJson.encode([my_addon]), status: 200 } @@ -523,7 +524,7 @@ module Heroku::Command WARNING: No add-on name specified (see `heroku help addons:upgrade`) Finding add-on from service my_service on app example... done Found my_addon (my_plan) on example. -Changing my_addon plan to my_service... done +Changing my_addon plan to my_service... done, (free) OUTPUT Excon.stubs.shift(2) @@ -561,7 +562,7 @@ module Heroku::Command allow(@addons.api).to receive(:request) { |args| expect(args[:method]).to eq :patch expect(args[:path]).to eq "/apps/example/addons/my_service" - }.and_return(stringify(addon)) + }.and_return(OpenStruct.new(body: stringify(addon))) @addons.downgrade end @@ -572,7 +573,7 @@ module Heroku::Command allow(@addons.api).to receive(:request) { |args| expect(args[:method]).to eq :patch expect(args[:path]).to eq "/apps/example/addons/my_service" - }.and_return(stringify(addon)) + }.and_return(OpenStruct.new(body: stringify(addon))) @addons.downgrade end @@ -602,7 +603,7 @@ module Heroku::Command stderr, stdout = execute("addons:downgrade my_service low_plan") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT -Changing my_service plan to low_plan... done +Changing my_service plan to low_plan... done, (free) OUTPUT end end From 9676ccb79b5e3ca014067fa408d6ad154726ab4c Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Tue, 19 May 2015 11:00:33 -0700 Subject: [PATCH 559/952] Display price on addons:destroy --- lib/heroku/command/addons.rb | 10 +++++++--- spec/heroku/command/addons_spec.rb | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index a224b46ca..9b6d119c9 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -308,15 +308,19 @@ def destroy addon_attachments = get_attachments(:resource => addon['id']) action("Destroying #{addon['name']} on #{app['name']}") do - api.request( + addon = api.request( :body => json_encode({ "force" => options[:force], }), :expects => 200..300, - :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.switzerland", + "Accept-Expansion" => "plan" + }, :method => :delete, :path => "/apps/#{app['id']}/addons/#{addon['id']}" - ) + ).body + @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') end if addon['config_vars'].any? # litmus test for whether the add-on's attachments have vars diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 00f9433de..21cfcd784 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -629,7 +629,7 @@ module Heroku::Command allow(@addons.api).to receive(:request) { |args| expect(args[:path]).to eq "/apps/123/addons/abc123" - } + }.and_return(OpenStruct.new(body: stringify(addon))) @addons.destroy end @@ -646,7 +646,7 @@ module Heroku::Command allow(@addons.api).to receive(:request) { |args| expect(args[:path]).to eq "/apps/123/addons/abc123" - } + }.and_return(OpenStruct.new(body: stringify(addon))) @addons.destroy end From 90e2e4d3a3be4eb0316524f559de7f74033a821a Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Tue, 19 May 2015 11:30:18 -0700 Subject: [PATCH 560/952] Remove switzerland variant from add-ons requests There's no harm in requesting now that the variant no longer exists, as API will fall back to v3, but this should avoid any confusion about needing to use the variant or not --- lib/heroku/command/addons.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 9b6d119c9..295e44584 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -180,7 +180,7 @@ def attach "name" => attachment_name }), :expects => [201, 422], - :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, :method => :post, :path => "/addon-attachments" ) @@ -217,7 +217,7 @@ def detach action("Removing #{attachment_name} attachment to #{addon_name} from #{app}") do api.request( :expects => 200..300, - :headers => { "Accept" => "application/vnd.heroku+json; version=3.switzerland" }, + :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, :method => :delete, :path => "/addon-attachments/#{addon_attachment['id']}" ).body @@ -262,7 +262,7 @@ def upgrade }), :expects => 200..300, :headers => { - "Accept" => "application/vnd.heroku+json; version=3.switzerland", + "Accept" => "application/vnd.heroku+json; version=3", "Accept-Expansion" => "plan" }, :method => :patch, @@ -314,7 +314,7 @@ def destroy }), :expects => 200..300, :headers => { - "Accept" => "application/vnd.heroku+json; version=3.switzerland", + "Accept" => "application/vnd.heroku+json; version=3", "Accept-Expansion" => "plan" }, :method => :delete, From d84134d5afd22e7c534064a379bea09ffe24d806 Mon Sep 17 00:00:00 2001 From: Michael Baudino Date: Fri, 22 May 2015 14:10:58 +0200 Subject: [PATCH 561/952] Add --force-colors to logs command so we can `grep -v` and still have colors for exemple --- lib/heroku/command/logs.rb | 3 ++- lib/heroku/helpers/log_displayer.rb | 23 ++++++++++------------- spec/heroku/command/logs_spec.rb | 14 +++++++++++++- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/heroku/command/logs.rb b/lib/heroku/command/logs.rb index f4c7b776f..efe581d66 100644 --- a/lib/heroku/command/logs.rb +++ b/lib/heroku/command/logs.rb @@ -13,6 +13,7 @@ class Heroku::Command::Logs < Heroku::Command::Base # -p, --ps PS # only display logs from the given process # -s, --source SOURCE # only display logs from the given source # -t, --tail # continually stream logs + # --force-colors # Force use of ANSI color characters (even on non-tty outputs) # #Example: # @@ -29,7 +30,7 @@ def index opts << "ps=#{URI.encode(options[:ps])}" if options[:ps] opts << "source=#{URI.encode(options[:source])}" if options[:source] - log_displayer = ::Heroku::Helpers::LogDisplayer.new(heroku, app, opts) + log_displayer = ::Heroku::Helpers::LogDisplayer.new(heroku, app, opts, options[:force_colors]) log_displayer.display_logs end diff --git a/lib/heroku/helpers/log_displayer.rb b/lib/heroku/helpers/log_displayer.rb index 50ba95e13..5cd41234a 100644 --- a/lib/heroku/helpers/log_displayer.rb +++ b/lib/heroku/helpers/log_displayer.rb @@ -5,10 +5,10 @@ class LogDisplayer include Heroku::Helpers - attr_reader :heroku, :app, :opts + attr_reader :heroku, :app, :opts, :force_colors - def initialize(heroku, app, opts) - @heroku, @app, @opts = heroku, app, opts + def initialize(heroku, app, opts, force_colors = false) + @heroku, @app, @opts, @force_colors = heroku, app, opts, force_colors end def display_logs @@ -17,19 +17,11 @@ def display_logs @token = nil heroku.read_logs(app, opts) do |chunk| - unless chunk.empty? - if STDOUT.isatty && ENV.has_key?("TERM") - display(colorize(chunk)) - else - display(chunk) - end - end + display(display_colors? ? colorize(chunk) : chunk) unless chunk.empty? end rescue Errno::EPIPE rescue Interrupt => interrupt - if STDOUT.isatty && ENV.has_key?("TERM") - display("\e[0m") - end + display("\e[0m") if display_colors? raise(interrupt) end @@ -66,5 +58,10 @@ def parse_log(log) [1, 2, 4].map { |i| parsed[i] } end + private + + def display_colors? + force_colors || (STDOUT.isatty && ENV.has_key?("TERM")) + end end end diff --git a/spec/heroku/command/logs_spec.rb b/spec/heroku/command/logs_spec.rb index 9e69ab5c2..06a0eb500 100644 --- a/spec/heroku/command/logs_spec.rb +++ b/spec/heroku/command/logs_spec.rb @@ -54,7 +54,19 @@ STDOUT ENV["TERM"] = term end + + it "uses ansi if --force-colors is passed, even if stdout is not a tty and TERM is not set" do + old_term = ENV.delete("TERM") + old_stdout_isatty = $stdout.isatty + stub($stdout).isatty.returns(false) + stderr, stdout = execute("logs --force-colors") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +\e[36m2011-01-01T00:00:00+00:00 app[web.1]:\e[0m test +STDOUT + ENV["TERM"] = old_term + stub($stdout).isatty.returns(old_stdout_isatty) + end end end - end From 8fe9bb84710b5aece000357983cd31fcb974a3c5 Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Tue, 26 May 2015 14:28:46 -0700 Subject: [PATCH 562/952] Tell you where youre copying to an from in pg:copy --- lib/heroku/command/pg_backups.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index ae8edd070..95ec0ee5c 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -23,7 +23,11 @@ def copy attachment = target.attachment || source.attachment - if confirm_command + message = "WARNING: Destructive Action" + message << "\nTransferring data from #{source.name} to #{target.name}" + message << "\nThis command will affect the app: #{app}" + + if confirm_command(app, message) xfer = hpg_client(attachment).pg_copy(source.name, source.url, target.name, target.url) poll_transfer('copy', xfer[:uuid]) From c81effa113d78be158dccbc1b621f2f5aa0c925a Mon Sep 17 00:00:00 2001 From: Rimas Silkaitis Date: Tue, 26 May 2015 15:08:00 -0700 Subject: [PATCH 563/952] tweak language --- lib/heroku/command/pg_backups.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 95ec0ee5c..1f411cf82 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -24,7 +24,8 @@ def copy attachment = target.attachment || source.attachment message = "WARNING: Destructive Action" - message << "\nTransferring data from #{source.name} to #{target.name}" + message << "\nThis command will remove all data from #{target.name}" + message << "\nData from #{source.name} will then be transferred to #{target.name}" message << "\nThis command will affect the app: #{app}" if confirm_command(app, message) From 2ce77935853d01babc1852a5df2d5848a0b8e668 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 27 May 2015 11:10:08 -0700 Subject: [PATCH 564/952] v3.37.2 --- CHANGELOG | 7 +++++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d3ed028f9..9c278118f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.37.2 2015-05-27 +================= +Made confirmation language clearer for pg:copy +Add --force-colors to logs +Removed switzerland variant +Display price of add-on plan on some commands + 3.37.1 2015-05-18 ================= Hide hidden commands diff --git a/Gemfile.lock b/Gemfile.lock index 9aa0aa496..893477894 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.1) + heroku (3.37.2) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.45.1) + excon (0.45.2) fakefs (0.6.7) heroku-api (0.3.23) excon (~> 0.44) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 692961c5c..bdbd012a3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.1" + VERSION = "3.37.2" end From 28bfe3c744221d6a8b99eb50eeaef2f749b23cb4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 29 May 2015 09:25:47 -0700 Subject: [PATCH 565/952] added arm support --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index b54cb283b..08d6deeb9 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -144,6 +144,8 @@ def self.arch case RbConfig::CONFIG['host_cpu'] when /x86_64/ "amd64" + when "arm" + "arm" else "386" end From 0122cfa71c16bd413a85747ae1886ffc1f2ce605 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 29 May 2015 09:26:39 -0700 Subject: [PATCH 566/952] v3.37.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9c278118f..418aa93a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.37.3 2015-05-28 +================= +Added arm support for Toolbelt v4 + 3.37.2 2015-05-27 ================= Made confirmation language clearer for pg:copy diff --git a/Gemfile.lock b/Gemfile.lock index 893477894..1a63b1378 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.2) + heroku (3.37.3) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index bdbd012a3..4ad53231e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.2" + VERSION = "3.37.3" end From 78ce54bead26e3164aea78f30931f640aed5d391 Mon Sep 17 00:00:00 2001 From: tef Date: Fri, 29 May 2015 12:12:58 -0700 Subject: [PATCH 567/952] Use created_at to find last backup, not number --- lib/heroku/command/pg_backups.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 1f411cf82..5e6cb799a 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -248,7 +248,7 @@ def backup_status backups = client.transfers last_backup = backups.select do |b| b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' - end.sort_by { |b| b[:num] }.last + end.sort_by { |b| b[:created_at] }.last if last_backup.nil? error("No backups. Capture one with `heroku pg:backups capture`.") else @@ -471,7 +471,7 @@ def public_url else last_successful_backup = client.transfers.select do |xfer| xfer[:succeeded] && xfer[:to_type] == 'gof3r' - end.sort_by { |b| b[:num] }.last + end.sort_by { |b| b[:created_at] }.last if last_successful_backup.nil? error("No backups. Capture one with `heroku pg:backups capture`.") else From b6eb7c5126ac0d6590f5343d803323db7f5e2fe5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 29 May 2015 13:09:21 -0700 Subject: [PATCH 568/952] fix warnings --- lib/heroku/command/config.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index 0c2d671aa..c5f516c91 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -73,10 +73,10 @@ def set error("Usage: heroku config:set KEY1=VALUE1 [KEY2=VALUE2 ...]\nMust specify KEY and VALUE to set.") end - vars = args.inject({}) do |vars, arg| + vars = args.inject({}) do |v, arg| key, value = arg.split('=', 2) - vars[key] = value - vars + v[key] = value + v end action("Setting config vars and restarting #{app}") do @@ -86,7 +86,7 @@ def set if release = api.get_release(app, 'current').body release['name'] end - rescue Heroku::API::Errors::RequestFailed => e + rescue Heroku::API::Errors::RequestFailed end end @@ -147,7 +147,7 @@ def unset if release = api.get_release(app, 'current').body release['name'] end - rescue Heroku::API::Errors::RequestFailed => e + rescue Heroku::API::Errors::RequestFailed end end end From efe5cb6d06874e32b651a1a7590672cc1575c196 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 29 May 2015 13:09:29 -0700 Subject: [PATCH 569/952] preauth for config:unset --- lib/heroku/command/config.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/config.rb b/lib/heroku/command/config.rb index c5f516c91..72e322439 100644 --- a/lib/heroku/command/config.rb +++ b/lib/heroku/command/config.rb @@ -135,6 +135,7 @@ def get # Unsetting B and restarting example... done, v124 # def unset + requires_preauth if args.empty? error("Usage: heroku config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset.") end From 944cfed4ffe4f3d73bef742d101a0d889b252ae6 Mon Sep 17 00:00:00 2001 From: tef Date: Fri, 29 May 2015 13:39:31 -0700 Subject: [PATCH 570/952] Allow client to cancel by transfer name --- lib/heroku/command/pg_backups.rb | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 1f411cf82..be787cf4b 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -45,7 +45,7 @@ def copy # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) # public-url BACKUP_ID # get secret but publicly accessible URL for BACKUP_ID to download it # -q, --quiet # Hide expiration message (for use in scripts) - # cancel # cancel an in-progress backup + # cancel [BACKUP_ID] # cancel an in-progress backup or restore (default newest) # delete BACKUP_ID # delete an existing backup # schedule DATABASE # schedule nightly backups for given database # --at ':00 ' # at a specific (24h clock) hour in the given timezone @@ -491,10 +491,27 @@ def public_url end def cancel_backup + backup_name = shift_argument validate_arguments! client = hpg_app_client(app) - transfer = client.transfers.find { |b| b[:finished_at].nil? } + + transfer = if backup_name + backup_num = transfer_num(backup_name) + if backup_num.nil? + error("No such backup/restore: #{backup_name}") + else + client.transfers_get(backup_num) + end + else + last_transfer = client.transfers.sort_by { |b| b[:created_at] }.reverse.find { |b| b[:finished_at].nil? } + if last_transfer.nil? + error("No active backups/restores") + else + last_transfer + end + end + client.transfers_cancel(transfer[:uuid]) display "Canceled #{transfer_name(transfer)}" end From fd58a970395a1552a2942b44c92f22523b37b246 Mon Sep 17 00:00:00 2001 From: Bharat Mediratta Date: Fri, 29 May 2015 19:12:51 -0700 Subject: [PATCH 571/952] Improve 'ps scale' to handle dyno types that have non-alphanumeric characters in them (eg: Standard-1X) Fix for #1586. --- lib/heroku/command/ps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 3ef96e195..7130d4e2e 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -229,7 +229,7 @@ def scale change_map = {} changes = args.map do |arg| - if change = arg.scan(/^([a-zA-Z0-9_]+)([=+-]\d+)(?::(\w+))?$/).first + if change = arg.scan(/^([a-zA-Z0-9_]+)([=+-]\d+)(?::([\w-]+))?$/).first formation, quantity, size = change quantity.gsub!("=", "") # only allow + and - on quantity change_map[formation] = [quantity, size] From 01bfcc878e07e5bf34f337118fc3383eb611900c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 1 Jun 2015 14:37:29 -0700 Subject: [PATCH 572/952] use arm when host_cpu is armv7l --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 08d6deeb9..269965fce 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -144,7 +144,7 @@ def self.arch case RbConfig::CONFIG['host_cpu'] when /x86_64/ "amd64" - when "arm" + when /arm/ "arm" else "386" From 674a1c4765b583327e28b7c9575396fb4f7f2e42 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 1 Jun 2015 16:22:35 -0700 Subject: [PATCH 573/952] use v4 for auth:whoami --- lib/heroku/command/auth.rb | 7 ++----- spec/heroku/command/auth_spec.rb | 12 ------------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/lib/heroku/command/auth.rb b/lib/heroku/command/auth.rb index 4ace4b344..26b8cdd4c 100644 --- a/lib/heroku/command/auth.rb +++ b/lib/heroku/command/auth.rb @@ -77,10 +77,7 @@ def token # email@example.com # def whoami - validate_arguments! - - display Heroku::Auth.user + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('whoami', nil, ARGV[1..-1]) end - end - diff --git a/spec/heroku/command/auth_spec.rb b/spec/heroku/command/auth_spec.rb index b5b981808..b8ff58807 100644 --- a/spec/heroku/command/auth_spec.rb +++ b/spec/heroku/command/auth_spec.rb @@ -23,16 +23,4 @@ STDOUT end end - - describe "auth:whoami" do - it "displays the user's email address" do - stderr, stdout = execute("auth:whoami") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -email@example.com -STDOUT - end - - end - end From 38b015ae3898a62ea7aab2fcbc637c8201f1002e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 1 Jun 2015 16:28:31 -0700 Subject: [PATCH 574/952] v3.37.4 --- CHANGELOG | 9 +++++++++ lib/heroku/version.rb | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 418aa93a9..037859f6b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +3.37.4 2015-06-01 +================= +Use toolbelt v4 for auth:whoami +Fix ps:scale with Standard-1X dynos +Fix arm support on some machines +Allow pg:backups cancel to take an optional transfer name +Fix sorting on pg:backups +Fixed config:unset for paranoid apps + 3.37.3 2015-05-28 ================= Added arm support for Toolbelt v4 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4ad53231e..fbe271bdb 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.3" + VERSION = "3.37.4" end From a04ab22d11625a664615880203e4b8e5f33f68b4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 1 Jun 2015 16:36:22 -0700 Subject: [PATCH 575/952] v3.37.5 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 037859f6b..4966f5c68 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.37.5 2015-06-01 +================= +Fixed auth:whoami on first run + 3.37.4 2015-06-01 ================= Use toolbelt v4 for auth:whoami diff --git a/Gemfile.lock b/Gemfile.lock index 1a63b1378..252171f4c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.3) + heroku (3.37.5) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index fbe271bdb..7a3bd5dd7 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.4" + VERSION = "3.37.5" end From d3cd88397772a35f611329c190772597329270f1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 1 Jun 2015 16:42:42 -0700 Subject: [PATCH 576/952] check if logged in before releasing --- Rakefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Rakefile b/Rakefile index dbdbd0817..8162d22e4 100644 --- a/Rakefile +++ b/Rakefile @@ -33,6 +33,7 @@ task :can_release do $stderr.puts "cannot release, #{version}, HEROKU_RELEASE_ACCESS and HEROKU_RELEASE_SECRET must be set" exit(1) end + system './bin/heroku auth:whoami' or exit 1 if `gem list ^heroku$ --remote` == "heroku (#{version})\n" $stderr.puts "cannot release #{version}, v#{version} is already released" exit(1) From 7195bf3dbc5b619eb38d4e72684f91a11f6e90de Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 1 Jun 2015 18:47:11 -0700 Subject: [PATCH 577/952] Handle backup schedule gracefully when app has no databases; add specs --- lib/heroku/command/pg_backups.rb | 3 ++ spec/heroku/command/pg_backups_spec.rb | 41 ++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 13acae9dc..9c3606c66 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -576,6 +576,9 @@ def unschedule_backups def list_schedules validate_arguments! attachment = arbitrary_app_db + if attachment.nil? + abort("#{app} has no heroku-postgresql databases.") + end schedules = hpg_client(attachment).schedules if schedules.empty? diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index ee62208e4..8a6b5f980 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -122,6 +122,47 @@ module Heroku::Command end end + describe "heroku pg:backups schedules" do + let(:schedules) do + [ { name: 'HEROKU_POSTGRESQL_GREEN_URL', + uuid: 'ffffffff-ffff-ffff-ffff-ffffffffffff', + hour: 4, timezone: 'US/Pacific' }, + { name: 'DATABASE_URL', + uuid: 'ffffffff-ffff-ffff-ffff-fffffffffffe', + hour: 20, timezone: 'UTC' } ] + end + + it "lists the existing schedules" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + stub_pg.schedules.returns(schedules) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to be_empty + expect(stdout).to eq(<<-EOF) +=== Backup Schedules +HEROKU_POSTGRESQL_GREEN_URL: daily at 4:00 (US/Pacific) +DATABASE_URL: daily at 20:00 (UTC) +EOF + end + + it "reports there are no schedules when none exist" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + stub_pg.schedules.returns([]) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to be_empty + expect(stdout).to match(/No backup schedules found/) + end + + it "reports there are no databases when the app has none" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return([]) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to match(/example has no heroku-postgresql databases/) + expect(stdout).to be_empty + end + end + describe "heroku pg:backups unschedule" do let(:schedules) do [ { name: 'HEROKU_POSTGRESQL_GREEN_URL', From 024417f93c800ada2a8c1f8d3e21c580a65be3d9 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 2 Jun 2015 13:09:20 -0700 Subject: [PATCH 578/952] only force encoding on old version of ruby --- lib/heroku/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 74f87ad6c..5ef2637a5 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -6,7 +6,7 @@ module Helpers extend self def home_directory - if running_on_windows? + if running_on_windows? && RUBY_VERSION == '1.9.3' # https://bugs.ruby-lang.org/issues/10126 Dir.home.force_encoding('cp775') else From 3ce4430b58245c0d7fb9077cc71f16933076ed33 Mon Sep 17 00:00:00 2001 From: tef Date: Tue, 2 Jun 2015 17:22:19 -0700 Subject: [PATCH 579/952] Sleep longer to avoid exhausting api quota --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 9c3606c66..5a1db5af8 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -426,7 +426,7 @@ def poll_transfer(action, transfer_id) raise end end - sleep 1 + sleep 3 end until backup[:finished_at] if backup[:succeeded] redisplay "#{action.capitalize} completed\n" From 1816db73f7bd8a9f79119969b21d45430f0b2788 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 3 Jun 2015 10:54:29 -0700 Subject: [PATCH 580/952] v3.37.6 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4966f5c68..a338ea73f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.37.6 2015-06-03 +================= +Fix non-ascii windows username issue for Ruby 2.0+ +Increased pgbackups polling sleep interval to prevent rate limiting + 3.37.5 2015-06-01 ================= Fixed auth:whoami on first run diff --git a/Gemfile.lock b/Gemfile.lock index 252171f4c..3e2624e9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.5) + heroku (3.37.6) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 7a3bd5dd7..77b1f61be 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.5" + VERSION = "3.37.6" end From 44dcec29ee05a4a5093fd62946e5cfdb7b9552e5 Mon Sep 17 00:00:00 2001 From: tef Date: Wed, 3 Jun 2015 18:37:02 -0700 Subject: [PATCH 581/952] Use correct app name in warnings, allow using app::backup for restore source --- lib/heroku/command/pg_backups.rb | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 5a1db5af8..7b60328d8 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -26,9 +26,9 @@ def copy message = "WARNING: Destructive Action" message << "\nThis command will remove all data from #{target.name}" message << "\nData from #{source.name} will then be transferred to #{target.name}" - message << "\nThis command will affect the app: #{app}" + message << "\nThis command will affect the app: #{attachment.app}" - if confirm_command(app, message) + if confirm_command(attachment.app, message) xfer = hpg_client(attachment).pg_copy(source.name, source.url, target.name, target.url) poll_transfer('copy', xfer[:uuid]) @@ -369,8 +369,12 @@ def restore_backup restore_url = restore_from else # assume we're restoring from a backup - backup_name = restore_from - backups = hpg_app_client(app).transfers.select do |b| + if restore_from =~ /::/ + backup_app, backup_name = restore_from.split('::') + else + backup_app, backup_name = [app, restore_from] + end + backups = hpg_app_client(backup_app).transfers.select do |b| b[:from_type] == 'pg_dump' && b[:to_type] == 'gof3r' end backup = if backup_name == :latest @@ -380,16 +384,16 @@ def restore_backup backups.find { |b| transfer_name(b) == backup_name } end if backups.empty? - abort("No backups. Capture one with `heroku pg:backups capture`.") + abort("No backups for #{backup_app}. Capture one with `heroku pg:backups capture`.") elsif backup.nil? - abort("Backup #{backup_name} not found.") + abort("Backup #{backup_name} not found for #{backup_app}.") elsif !backup[:succeeded] - abort("Backup #{backup_name} did not complete successfully; cannot restore it.") + abort("Backup #{backup_name} for #{backup_app} did not complete successfully; cannot restore it.") end restore_url = backup[:to_url] end - if confirm_command + if confirm_command(attachment.app) restore = hpg_client(attachment).backups_restore(restore_url) display <<-EOF Use Ctrl-C at any time to stop monitoring progress; the backup @@ -397,7 +401,6 @@ def restore_backup Stop a running restore with heroku pg:backups cancel. #{transfer_name(restore)} ---restore---> #{attachment.name} - EOF poll_transfer('restore', restore[:uuid]) end From f29054b965f300b6a2eeb72bbf313b1704ec3fdf Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 7 Jun 2015 20:05:00 -0700 Subject: [PATCH 582/952] show error even if there is an error while printing it --- lib/heroku/helpers.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 5ef2637a5..fbf222412 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -448,6 +448,8 @@ def styled_error(error, message='Heroku client internal error.') rollbar_id = Rollbar.error(error) $stderr.puts(format_error(error, message, rollbar_id)) error_log(message, error.message, error.backtrace.join("\n")) + rescue => e + $stderr.puts e, e.backtrace, error, error.backtrace end def error_log(*obj) From 4a6d94ed4db41c3c0aa89b2f0b53a01fba39bfa0 Mon Sep 17 00:00:00 2001 From: tef Date: Mon, 8 Jun 2015 16:01:53 -0700 Subject: [PATCH 583/952] Add in Z, EU timezones, force schedule to take an argument --- lib/heroku/command/pg_backups.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 7b60328d8..4360de408 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -522,7 +522,12 @@ def cancel_backup def schedule_backups db = shift_argument validate_arguments! - at = options[:at] || '04:00 UTC' + + at = options[:at] + if !at + error("You must specifiy a time to schedule backups, i.e --at '04:00 UTC'") + end + schedule_opts = parse_schedule_time(at) resolver = generate_resolver @@ -612,7 +617,10 @@ def parse_schedule_time(time_str) 'CST' => 'America/Chicago', 'CDT' => 'America/Chicago', 'EST' => 'America/New_York', - 'EDT' => 'America/New_York' + 'EDT' => 'America/New_York', + 'Z' => 'UTC', + 'GMT' => 'Europe/London', + 'BST' => 'Europe/London', } if remap_tzs.has_key? tz.upcase tz = remap_tzs[tz.upcase] From b57a1b90529dbad191ec96b409c392cf7c13e5d2 Mon Sep 17 00:00:00 2001 From: Rhys Elsmore Date: Mon, 8 Jun 2015 16:16:47 -0700 Subject: [PATCH 584/952] Now using HTTPS :sparkles: --- tasks/pkg.rake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/pkg.rake b/tasks/pkg.rake index ff84a6edd..be29192e5 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -35,13 +35,13 @@ file dist("heroku-toolbelt-#{version}.pkg") => distribution_files("pkg") do |t| end unless File.exists?(dist('foreman-0.75.0.pkg')) - sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/foreman-0.75.0.pkg -o #{dist('foreman-0.75.0.pkg')} } + sh %{ curl https://heroku-toolbelt.s3.amazonaws.com/foreman-0.75.0.pkg -o #{dist('foreman-0.75.0.pkg')} } end sh %{ pkgutil --expand #{dist('foreman-0.75.0.pkg')} foreman } mv "foreman/foreman.pkg", "pkg/foreman.pkg" unless File.exists?(dist('ruby.pkg')) - sh %{ curl http://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o #{dist('ruby.pkg')} } + sh %{ curl https://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o #{dist('ruby.pkg')} } end sh %{ pkgutil --expand #{dist('ruby.pkg')} ruby } mv "ruby/ruby-1.9.3-p194.pkg", "pkg/ruby.pkg" From c05643ebdef2607f3b28e75e723f5bb6e7945710 Mon Sep 17 00:00:00 2001 From: Rhys Elsmore Date: Mon, 8 Jun 2015 16:20:27 -0700 Subject: [PATCH 585/952] Using HTTPS --- tasks/exe.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/exe.rake b/tasks/exe.rake index 70bebf3a9..64cfa3495 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -44,7 +44,7 @@ end def cache_file_from_bucket(filename) FileUtils.mkdir_p $cache_path file_cache_path = File.join($cache_path, filename) - system "curl -# http://heroku-toolbelt.s3.amazonaws.com/#{filename} -o '#{file_cache_path}'" unless File.exists? file_cache_path + system "curl -# https://heroku-toolbelt.s3.amazonaws.com/#{filename} -o '#{file_cache_path}'" unless File.exists? file_cache_path file_cache_path end From 6b946d2ff04726c88fa840ae23f9a0222dd681db Mon Sep 17 00:00:00 2001 From: tef Date: Tue, 9 Jun 2015 11:31:39 -0700 Subject: [PATCH 586/952] Use better target name for confirmation --- lib/heroku/command/pg_backups.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 4360de408..99a7ec938 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -23,12 +23,22 @@ def copy attachment = target.attachment || source.attachment + if target.attachment.nil? + target_url = URI.parse(target.url) + confirm_with = target_url.path[1..-1] + confirm_with = target_url.host if confirm_with.empty? + affected = target.name.downcase + else + confirm_with = target.attachment.app + affected = "the app #{target.attachment.app}" + end + message = "WARNING: Destructive Action" message << "\nThis command will remove all data from #{target.name}" message << "\nData from #{source.name} will then be transferred to #{target.name}" - message << "\nThis command will affect the app: #{attachment.app}" + message << "\nThis command will affect #{affected}" - if confirm_command(attachment.app, message) + if confirm_command(confirm_with, message) xfer = hpg_client(attachment).pg_copy(source.name, source.url, target.name, target.url) poll_transfer('copy', xfer[:uuid]) From e57f17ce79fe5309faf7bcdbaa9e76654d7abcec Mon Sep 17 00:00:00 2001 From: tef Date: Wed, 10 Jun 2015 15:18:55 -0700 Subject: [PATCH 587/952] Missing colon --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 99a7ec938..971f1bbbf 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -30,7 +30,7 @@ def copy affected = target.name.downcase else confirm_with = target.attachment.app - affected = "the app #{target.attachment.app}" + affected = "the app: #{target.attachment.app}" end message = "WARNING: Destructive Action" From 722074d17f02a5ec0ccbc77e6c8eb0e00af68430 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 11 Jun 2015 17:16:13 -0700 Subject: [PATCH 588/952] v3.37.7 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a338ea73f..93667d19c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.37.7 2015-06-11 +================= +pgbackups bug fixes https://github.com/heroku/heroku/pull/1604 +Ensure errors are shown when there is an error displaying the error + 3.37.6 2015-06-03 ================= Fix non-ascii windows username issue for Ruby 2.0+ diff --git a/Gemfile.lock b/Gemfile.lock index 3e2624e9c..eded6077b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.6) + heroku (3.37.7) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 77b1f61be..f12520211 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.6" + VERSION = "3.37.7" end From a8feec56d703b8afeaf1c40871eb8168c070193f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 13 Jun 2015 16:05:07 -0700 Subject: [PATCH 589/952] remove foreman --- resources/pkg/Distribution.erb | 5 ----- resources/pkg/postinstall | 2 +- tasks/pkg.rake | 6 ------ 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/resources/pkg/Distribution.erb b/resources/pkg/Distribution.erb index 43638a952..7c792fdbb 100644 --- a/resources/pkg/Distribution.erb +++ b/resources/pkg/Distribution.erb @@ -4,17 +4,12 @@ - - - - - #foreman.pkg #heroku-client.pkg #ruby.pkg diff --git a/resources/pkg/postinstall b/resources/pkg/postinstall index f9cb2a062..59cf5f6ad 100755 --- a/resources/pkg/postinstall +++ b/resources/pkg/postinstall @@ -42,4 +42,4 @@ case $(which heroku) in esac # symlink binary to /usr/bin/heroku -ln -sf /usr/local/heroku/bin/heroku /usr/bin/heroku +sudo ln -sf /usr/local/heroku/bin/heroku /usr/bin/heroku diff --git a/tasks/pkg.rake b/tasks/pkg.rake index be29192e5..1cde72bdb 100644 --- a/tasks/pkg.rake +++ b/tasks/pkg.rake @@ -34,12 +34,6 @@ file dist("heroku-toolbelt-#{version}.pkg") => distribution_files("pkg") do |t| sh %{ find . | cpio -o --format odc | gzip -c > ../pkg/heroku-client.pkg/Payload } end - unless File.exists?(dist('foreman-0.75.0.pkg')) - sh %{ curl https://heroku-toolbelt.s3.amazonaws.com/foreman-0.75.0.pkg -o #{dist('foreman-0.75.0.pkg')} } - end - sh %{ pkgutil --expand #{dist('foreman-0.75.0.pkg')} foreman } - mv "foreman/foreman.pkg", "pkg/foreman.pkg" - unless File.exists?(dist('ruby.pkg')) sh %{ curl https://heroku-toolbelt.s3.amazonaws.com/ruby.pkg -o #{dist('ruby.pkg')} } end From 8003c04ed7aa79e8d3a100ff7c05001c707ee90f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 13 Jun 2015 16:18:13 -0700 Subject: [PATCH 590/952] fix el capitan installs --- resources/pkg/postinstall | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/pkg/postinstall b/resources/pkg/postinstall index 59cf5f6ad..0cc64407d 100755 --- a/resources/pkg/postinstall +++ b/resources/pkg/postinstall @@ -42,4 +42,4 @@ case $(which heroku) in esac # symlink binary to /usr/bin/heroku -sudo ln -sf /usr/local/heroku/bin/heroku /usr/bin/heroku +sudo ln -sf /usr/local/heroku/bin/heroku /usr/local/bin/heroku From 997739b2f84250ddef56cd15013bde1b590b6286 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 13 Jun 2015 16:20:01 -0700 Subject: [PATCH 591/952] removed foreman from deb and exe --- resources/deb/heroku-toolbelt/control | 2 +- resources/exe/foreman | 9 --------- resources/exe/foreman.bat | 11 ----------- resources/exe/heroku.iss | 3 --- tasks/deb.rake | 21 +-------------------- tasks/exe.rake | 2 -- 6 files changed, 2 insertions(+), 46 deletions(-) delete mode 100755 resources/exe/foreman delete mode 100644 resources/exe/foreman.bat diff --git a/resources/deb/heroku-toolbelt/control b/resources/deb/heroku-toolbelt/control index 726cef4ae..99dd40e80 100644 --- a/resources/deb/heroku-toolbelt/control +++ b/resources/deb/heroku-toolbelt/control @@ -3,7 +3,7 @@ Version: <%= version %> Section: main Priority: standard Architecture: all -Depends: git-core, foreman, heroku (= <%= version %>) +Depends: git-core, heroku (= <%= version %>) Installed-Size: Maintainer: Heroku Description: A metapackage for working with the Heroku platform. diff --git a/resources/exe/foreman b/resources/exe/foreman deleted file mode 100755 index 2fee762fe..000000000 --- a/resources/exe/foreman +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -# find embedded ruby relative to script -bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` -exec "$bindir/ruby" -x "$0" "$@" - -#!/usr/bin/env ruby -# encoding: UTF-8 -require "foreman/cli" -Foreman::CLI.start diff --git a/resources/exe/foreman.bat b/resources/exe/foreman.bat deleted file mode 100644 index f15c848c6..000000000 --- a/resources/exe/foreman.bat +++ /dev/null @@ -1,11 +0,0 @@ -:: Don't use ECHO OFF to avoid possible change of ECHO -:: Use SETLOCAL so variables set in the script are not persisted -@SETLOCAL - -:: Add bundled ruby version to the PATH, use HerokuPath as starting point -@SET HEROKU_RUBY="%HerokuPath%\ruby-1.9.3\bin" -@SET PATH=%HEROKU_RUBY%;%PATH% - -:: Invoke 'foreman' (the calling script) as argument to ruby. -:: Also forward all the arguments provided to it. -@ruby.exe "%~dpn0" %* diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 0c2af35bc..4a1046819 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -25,7 +25,6 @@ Name: custom; Description: "Custom Installation"; flags: iscustom [Components] Name: "toolbelt"; Description: "Heroku Toolbelt"; Types: "client custom" Name: "toolbelt/client"; Description: "Heroku Client"; Types: "client custom"; Flags: fixed -Name: "toolbelt/foreman"; Description: "Foreman"; Types: "client custom" Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git.exe')" Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git.exe')" @@ -45,8 +44,6 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen [Run] Filename: "{tmp}\rubyinstaller.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-1.9.3"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" -Filename: "{app}\ruby-1.9.3\bin\gem.bat"; Parameters: "install foreman --no-rdoc --no-ri"; \ - Flags: runhidden shellexec waituntilterminated; StatusMsg: "Installing Foreman"; Components: "toolbelt/foreman" Filename: "{tmp}\git.exe"; Parameters: "/silent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" diff --git a/tasks/deb.rake b/tasks/deb.rake index f0e5cbe35..aecaefa37 100644 --- a/tasks/deb.rake +++ b/tasks/deb.rake @@ -1,5 +1,3 @@ -FOREMAN_VERSION = "0.75.0" - namespace :deb do desc "build deb" task :build => dist("heroku-toolbelt-#{version}.apt") @@ -9,7 +7,7 @@ namespace :deb do s3_store_dir dist("heroku-toolbelt-#{version}.apt"), "apt", "heroku-toolbelt" end - file dist("heroku-toolbelt-#{version}.apt") => [ dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-toolbelt-#{version}.deb") ] do |t| + file dist("heroku-toolbelt-#{version}.apt") => [ dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb"), dist("heroku-toolbelt-#{version}.apt/heroku-toolbelt-#{version}.deb") ] do |t| abort "Don't publish .debs of pre-releases!" if version =~ /[a-zA-Z]$/ cd t.name do |dir| @@ -23,23 +21,6 @@ namespace :deb do end - file dist("heroku-toolbelt-#{version}.apt/foreman-#{FOREMAN_VERSION}.deb") do |t| - mkdir_p File.dirname(t.name) - unless File.exist? "dist/foreman" - sh "git clone https://github.com/ddollar/foreman.git dist/foreman" - end - cd "dist/foreman" do - sh "git checkout v#{FOREMAN_VERSION}" - rm_rf ".bundle" - rm_rf "apt-#{FOREMAN_VERSION}" - Bundler.with_clean_env do - sh "unset GEM_HOME RUBYOPT; bundle install --path vendor/bundle" or abort - sh "unset GEM_HOME RUBYOPT; bundle exec rake deb:build" or abort - end - mv "pkg/apt-#{FOREMAN_VERSION}/foreman-#{FOREMAN_VERSION}.deb", t.name - end - end - file dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb") => distribution_files("deb") do |t| tempdir do mkdir_p "usr/local/heroku" diff --git a/tasks/exe.rake b/tasks/exe.rake index 64cfa3495..836959d2e 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -69,8 +69,6 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| # add windows helper executables to the heroku cli cp resource("exe/heroku.bat"), "#{heroku_cli_path}/bin/heroku.bat" cp resource("exe/heroku"), "#{heroku_cli_path}/bin/heroku" - cp resource("exe/foreman.bat"), "#{heroku_cli_path}/bin/foreman.bat" - cp resource("exe/foreman"), "#{heroku_cli_path}/bin/foreman" cp resource("exe/ssh-keygen.bat"), "#{heroku_cli_path}/bin/ssh-keygen.bat" # render the iss file used by inno setup to compile the installer From 030636484f3bb32b283af41a4978818655c60925 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 13 Jun 2015 16:37:56 -0700 Subject: [PATCH 592/952] remove PATH modification to just use /usr/local/bin symlink --- resources/pkg/postinstall | 42 --------------------------------------- 1 file changed, 42 deletions(-) diff --git a/resources/pkg/postinstall b/resources/pkg/postinstall index 0cc64407d..88f4bd82e 100755 --- a/resources/pkg/postinstall +++ b/resources/pkg/postinstall @@ -1,45 +1,3 @@ #!/bin/sh -usershell=$(dscl localhost -read /Local/Default/Users/$USER shell | sed -e 's/[^ ]* //') - -startup_files() { - case $(basename $usershell) in - zsh) - echo ".zlogin .zshrc .zprofile .zshenv" - ;; - bash) - echo ".bashrc .bash_profile .bash_login .profile" - ;; - *) - echo ".bash_profile .zshrc .profile" - ;; - esac -} - -install_path() { - for file in $(startup_files); do - [ -f $HOME/$file ] || continue - (grep "Added by the Heroku" $HOME/$file >/dev/null) && break - - cat <>$HOME/$file - -### Added by the Heroku Toolbelt -export PATH="/usr/local/heroku/bin:\$PATH" -MESSAGE - - # done after we add to one file - break - done -} - -# if the toolbelt is not returned by `which`, let's add to the PATH -case $(which heroku) in - /usr/bin/heroku|/usr/local/heroku/bin/heroku) - ;; - *) - install_path - ;; -esac - -# symlink binary to /usr/bin/heroku sudo ln -sf /usr/local/heroku/bin/heroku /usr/local/bin/heroku From d303701bbdd60e75d1c47fdc299b8332b7bfaf3f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 13 Jun 2015 16:05:34 -0700 Subject: [PATCH 593/952] make ps:scale automatically switch tiers In general, users shouldn't have to be aware of tiers and just be able to switch between types no matter what tier the type belongs to. This makes ps:scale automatically switch between tiers when using ps:scale to change dyno types. The one exception to this is that if there are existing dynos the user isn't transferring to a new type, we don't want to change the tier and effectively move *all* of their dynos to the lowest level on the new tier. This change will check to see if the user is trying to enter a state like that with some dynos on one tier and some dynos on another and error out. Wherever possible the code will simply continue to the API and allow it to show errors rather than add additional validation on the CLI which may be too strict. --- lib/heroku/command/ps.rb | 93 +++++++++++++++++++++++++++++++--- lib/heroku/helpers.rb | 5 ++ spec/heroku/command/ps_spec.rb | 13 +++-- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 7130d4e2e..faa138eb9 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -17,7 +17,6 @@ class Heroku::Command::Ps < Heroku::Command::Base end COSTS = Hash[*costs.flatten] - # ps:dynos [QTY] # # DEPRECATED: use `heroku ps:scale dynos=N` @@ -226,12 +225,13 @@ def restart # Scaling dynos... done, now running web at 3:2X, worker at 1:1X. # def scale + requires_preauth change_map = {} changes = args.map do |arg| if change = arg.scan(/^([a-zA-Z0-9_]+)([=+-]\d+)(?::([\w-]+))?$/).first formation, quantity, size = change - quantity.gsub!("=", "") # only allow + and - on quantity + quantity = quantity[1..-1].to_i if quantity[0] == "=" change_map[formation] = [quantity, size] { "process" => formation, "quantity" => quantity, "size" => size} end @@ -242,6 +242,8 @@ def scale end action("Scaling dynos") do + change_tier_if_needed(edge_app_formation.body, changes) + # The V3 API supports atomic scale+resize, so we make a raw request here # since the heroku-api gem still only supports V2. resp = api.request( @@ -381,10 +383,11 @@ def display_dyno_type_and_costs(app_resp, formation_resp) } end - # in case of an app not yet released - annotated = [tier_info] if annotated.empty? - - display_table(annotated, annotated.first.keys, annotated.first.keys) + if annotated.empty? + puts "No dynos active.\nUse `heroku ps:scale` to scale up dynos." + else + display_table(annotated, annotated.first.keys, annotated.first.keys) + end end def edge_app_info @@ -411,6 +414,84 @@ def edge_app_formation ) end + def change_tier_if_needed(formation, changes) + # first find what tier(s) the user wants to move to + # if there are multiple it will be caught later + new_tiers = tiers_in_formation(changes) + + # no tier specified, so we don't need to change anything + return if new_tiers.length == 0 + + # we have an unknown tier + # instead of validating in the CLI, just continue + # the API will error if there is an issue + return unless new_tiers.all? + + new_tier = new_tiers[0] + current_tier = tiers_in_formation(formation).first + + # the formation has no tier + # change the tier to the requested one + return patch_tier(new_tier) unless current_tier + + # tiers in formation and ps:scale are the same + # just continue + return if new_tier == current_tier + + # is the user changing all the dyno types to the new tier? + if consistent_tier_after_changes?(formation, changes) + # all dyno types are changing to the new tier so we can change to it + patch_tier(new_tier) + else + # some of the proposed changes are in a different tier than existing dyno types + # this is not allowed + from_type = formation[0]["size"] + to_type = changes[0]["size"] + error("Cannot mix #{to_type} with #{from_type} dynos.\nTo change all dynos to #{to_type}, run `heroku ps:type #{to_type}`.") + end + end + + def tiers_in_formation(formation) + tier_map = { + "free" => "free", + "hobby" => "hobby", + "standard-1x" => "production", + "standard-2x" => "production", + "performance" => "production", + } + formation + .select { |p| p["size"]} # types with a size + .select { |p| p["quantity"] > 0 } # types that have active dynos + .map { |p| p["size"]} # get the size of the type + .map { |s| tier_map[s.downcase]} # get the tier of the size + .uniq + end + + def consistent_tier_after_changes?(formation, changes) + formation = pretend_changes_to_formation(formation, changes) + tiers_in_formation(formation).length == 1 + end + + # return a new formation object with changes applied + # in-memory only, does not hit API + def pretend_changes_to_formation(formation, changes) + formation = deep_clone(formation) + changes.each do |change| + ps = formation.find{|p| p["type"] == change["process"]} + next unless ps + ps["size"] = change["size"] if change["size"] + q = change["quantity"] + if q.is_a? Fixnum + ps["quantity"] = q + elsif q.start_with? '+' + ps["quantity"] += q[1..-1].to_i + elsif q.start_with? '-' + ps["quantity"] -= q[1..-1].to_i + end + end + formation + end + def special_case_change_tier_and_resize(type) patch_tier("production") override_args = edge_app_formation.body.map { |ps| "#{ps['type']}=#{type}" } diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index fbf222412..216e057da 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -572,5 +572,10 @@ def has_http_git_entry_in_netrc def warn_if_using_jruby stderr_puts "WARNING: jruby is known to cause issues when used with the toolbelt." if RUBY_PLATFORM == "java" end + + # cheap deep clone + def deep_clone(obj) + json_decode(json_encode(obj)) + end end end diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index e126500bf..a829292c7 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -273,6 +273,7 @@ describe "ps:scale" do it "can scale using key/value format" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub({ :method => :patch, :path => "/apps/example/formation" }, { :body => [{"quantity" => "5", "size" => "1X", "type" => "web"}], :status => 200}) @@ -284,6 +285,7 @@ end it "can scale relative amounts" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub({ :method => :patch, :path => "/apps/example/formation" }, { :body => [{"quantity" => "3", "size" => "1X", "type" => "web"}], :status => 200}) @@ -295,11 +297,12 @@ end it "can resize while scaling" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "quantity" => "4", "size" => "2X"}] + "updates" => [{"process" => "web", "quantity" => 4, "size" => "2X"}] }.to_json }, :body => [{"quantity" => 4, "size" => "2X", "type" => "web"}], @@ -313,13 +316,14 @@ end it "can scale multiple types in one call" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "quantity" => "4", "size" => "1X"}, - {"process" => "worker", "quantity" => "2", "size" => "2x"}, + {"process" => "web", "quantity" => 4, "size" => "1X"}, + {"process" => "worker", "quantity" => 2, "size" => "2x"}, ] }.to_json }, @@ -338,11 +342,12 @@ end it "accepts PX as a valid size" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "quantity" => "4", "size" => "PX"}] + "updates" => [{"process" => "web", "quantity" => 4, "size" => "PX"}] }.to_json }, :body => [{"quantity" => 4, "size" => "PX", "type" => "web"}], From a0081b2b1e6b23d8c096d08ed69926658814aaf0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 15 Jun 2015 10:18:03 -0700 Subject: [PATCH 594/952] v3.38.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 93667d19c..cb9da9fe0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.38.0 2015-06-15 +================= +Fix ps:scale for new process types + 3.37.7 2015-06-11 ================= pgbackups bug fixes https://github.com/heroku/heroku/pull/1604 diff --git a/Gemfile.lock b/Gemfile.lock index eded6077b..6b2a66f27 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.37.7) + heroku (3.38.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f12520211..4fccc5921 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.37.7" + VERSION = "3.38.0" end From eca5de960b8cfb3403aea52d8dd7e51480535f32 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 15 Jun 2015 13:31:44 -0700 Subject: [PATCH 595/952] fix ps:type for new dyno types this also makes the output consistent for ps:type commands --- lib/heroku/command/ps.rb | 182 ++++++++------------------------- spec/heroku/command/ps_spec.rb | 50 +++++---- 2 files changed, 77 insertions(+), 155 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index faa138eb9..db489e38b 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -3,19 +3,7 @@ # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - PROCESS_TIERS =[ - {"tier"=>"free", "max_scale"=>1, "max_processes"=>2, "cost"=>{"Free"=>0}}, - {"tier"=>"hobby", "max_scale"=>1, "max_processes"=>nil, "cost"=>{"Hobby"=>700}}, - {"tier"=>"production", "max_scale"=>100, "max_processes"=>nil, "cost"=>{"Standard-1X"=>2500, "Standard-2X"=>5000, "Performance"=>50000}}, - {"tier"=>"traditional", "max_scale"=>100, "max_processes"=>nil, "cost"=>{"1X"=>3600, "2X"=>7200, "PX"=>57600}} - ] - - costs = PROCESS_TIERS.collect do |tier| - tier["cost"].collect do |name, cents_per_month| - [name, (cents_per_month / 100)] - end - end - COSTS = Hash[*costs.flatten] + COSTS = {"Free"=>0, "Hobby"=>7, "Standard-1X"=>25, "Standard-2X"=>50, "Performance"=>500, "1X"=>36, "2X"=>72, "PX"=>576} # ps:dynos [QTY] # @@ -226,14 +214,12 @@ def restart # def scale requires_preauth - change_map = {} changes = args.map do |arg| if change = arg.scan(/^([a-zA-Z0-9_]+)([=+-]\d+)(?::([\w-]+))?$/).first formation, quantity, size = change quantity = quantity[1..-1].to_i if quantity[0] == "=" - change_map[formation] = [quantity, size] - { "process" => formation, "quantity" => quantity, "size" => size} + { "type" => formation, "quantity" => quantity, "size" => size} end end.compact @@ -242,23 +228,8 @@ def scale end action("Scaling dynos") do - change_tier_if_needed(edge_app_formation.body, changes) - - # The V3 API supports atomic scale+resize, so we make a raw request here - # since the heroku-api gem still only supports V2. - resp = api.request( - :expects => 200, - :method => :patch, - :path => "/apps/#{app}/formation", - :body => json_encode("updates" => changes), - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Content-Type" => "application/json" - } - ) - new_scales = resp.body. - select {|p| change_map[p['type']] }. - map {|p| "#{p["type"]} at #{p["quantity"]}:#{p["size"]}" } + new_scales = scale_dynos(get_formation, changes) + .map {|p| "#{p["type"]} at #{p["quantity"]}:#{p["size"]}" } status("now running " + new_scales.join(", ") + ".") end end @@ -306,34 +277,29 @@ def stop # called with no arguments shows the current dyno type # # called with one argument sets the type - # where type is one of traditional|free|hobby|standard-1x|standard-2x|performance + # where type is one of free|hobby|standard-1x|standard-2x|performance # # called with 1..n DYNO=TYPE arguments sets the type per dyno - # this is only available when the app is on production and performance # def type - if args.any?{|arg| arg =~ /=/} - _original_resize - return - end - + requires_preauth app - process_tier = shift_argument - process_tier.downcase! if process_tier - validate_arguments! - - if %w[standard-1x standard-2x performance].include?(process_tier) - special_case_change_tier_and_resize(process_tier) - return - end - - # get or update app.process_tier - app_resp = process_tier.nil? ? edge_app_info : change_dyno_type(process_tier) - - # get, calculate and display app process type costs - formation_resp = edge_app_formation - - display_dyno_type_and_costs(app_resp, formation_resp) + formation = get_formation + changes = if args.any?{|arg| arg =~ /=/} + args.map do |arg| + if arg =~ /^([a-zA-Z0-9_]+)=([\w-]+)$/ + p = formation.find{|f| f["type"] == $1}.clone + p["size"] = $2 + p + end + end.compact + elsif args.any? + size = shift_argument.downcase + validate_arguments! + formation.map{|p| p["size"] = size; p} + end + scale_dynos(formation, changes) if changes + display_dyno_type_and_costs(get_formation) end alias_method :resize, :type @@ -341,21 +307,6 @@ def type private - def change_dyno_type(process_tier) - print "Changing dyno type... " - - app_resp = patch_tier(process_tier) - - if app_resp.status != 200 - puts "failed" - error app_resp.body["message"] + " Please use `heroku ps:scale` to change process size and scale." - end - - puts "done." - - return app_resp - end - def patch_tier(process_tier) api.request( :method => :patch, @@ -368,13 +319,9 @@ def patch_tier(process_tier) ) end - def display_dyno_type_and_costs(app_resp, formation_resp) - tier_info = PROCESS_TIERS.detect { |t| t["tier"] == app_resp.body["process_tier"] } - - formation = formation_resp.body.reject {|ps| ps['quantity'] < 1} - + def display_dyno_type_and_costs(formation) annotated = formation.sort_by{|d| d['type']}.map do |dyno| - cost = tier_info["cost"][dyno["size"]] * dyno["quantity"] / 100 + cost = COSTS[dyno["size"]] * dyno["quantity"] { 'dyno' => dyno['type'], 'type' => dyno['size'].rjust(4), @@ -384,25 +331,13 @@ def display_dyno_type_and_costs(app_resp, formation_resp) end if annotated.empty? - puts "No dynos active.\nUse `heroku ps:scale` to scale up dynos." + error "No process types on #{app}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile" else display_table(annotated, annotated.first.keys, annotated.first.keys) end end - def edge_app_info - api.request( - :expects => 200, - :method => :get, - :path => "/apps/#{app}", - :headers => { - "Accept" => "application/vnd.heroku+json; version=edge", - "Content-Type" => "application/json" - } - ) - end - - def edge_app_formation + def get_formation api.request( :expects => 200, :method => :get, @@ -411,7 +346,7 @@ def edge_app_formation "Accept" => "application/vnd.heroku+json; version=3", "Content-Type" => "application/json" } - ) + ).body end def change_tier_if_needed(formation, changes) @@ -477,64 +412,37 @@ def consistent_tier_after_changes?(formation, changes) def pretend_changes_to_formation(formation, changes) formation = deep_clone(formation) changes.each do |change| - ps = formation.find{|p| p["type"] == change["process"]} + ps = formation.find{|p| p["type"] == change["type"]} next unless ps ps["size"] = change["size"] if change["size"] q = change["quantity"] if q.is_a? Fixnum ps["quantity"] = q - elsif q.start_with? '+' + elsif q.start_with?('+') ps["quantity"] += q[1..-1].to_i - elsif q.start_with? '-' + elsif q.start_with?('-') ps["quantity"] -= q[1..-1].to_i end end formation end - def special_case_change_tier_and_resize(type) - patch_tier("production") - override_args = edge_app_formation.body.map { |ps| "#{ps['type']}=#{type}" } - _original_resize(override_args) - end - - def _original_resize(override_args=nil) - app - change_map = {} - - changes = (override_args || args).map do |arg| - if arg =~ /^([a-zA-Z0-9_]+)=([\w-]+)$/ - change_map[$1] = $2 - { "process" => $1, "size" => $2 } - end - end.compact - - if changes.empty? - message = [ - "Usage: heroku dyno:type DYNO1=1X|2X|PX [DYNO2=1X|2X|PX ...]", - "Must specify DYNO and TYPE to resize." - ] - error(message.join("\n")) - end - - resp = nil - action("Resizing and restarting the specified dynos") do - resp = api.request( - :expects => 200, - :method => :patch, - :path => "/apps/#{app}/formation", - :body => json_encode("updates" => changes), - :headers => { - "Accept" => "application/vnd.heroku+json; version=3", - "Content-Type" => "application/json" - } - ) - end + def scale_dynos(formation, changes) + change_tier_if_needed(formation, changes) - resp.body.select {|p| change_map.key?(p['type']) }.each do |p| - size = p["size"] - display "#{p["type"]} dynos now #{size} ($#{COSTS[size]}/month)" - end + # The V3 API supports atomic scale+resize, so we make a raw request here + # since the heroku-api gem still only supports V2. + resp = api.request( + :expects => 200, + :method => :patch, + :path => "/apps/#{app}/formation", + :body => json_encode("updates" => changes), + :headers => { + "Accept" => "application/vnd.heroku+json; version=3", + "Content-Type" => "application/json" + } + ) + resp.body.select {|p| changes.any?{|c| c["type"] == p["type"]} } end end diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index a829292c7..d622fbeb1 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -302,7 +302,7 @@ { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "quantity" => 4, "size" => "2X"}] + "updates" => [{"type" => "web", "quantity" => 4, "size" => "2X"}] }.to_json }, :body => [{"quantity" => 4, "size" => "2X", "type" => "web"}], @@ -322,8 +322,8 @@ :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "quantity" => 4, "size" => "1X"}, - {"process" => "worker", "quantity" => 2, "size" => "2x"}, + {"type" => "web", "quantity" => 4, "size" => "1X"}, + {"type" => "worker", "quantity" => 2, "size" => "2x"}, ] }.to_json }, @@ -347,7 +347,7 @@ { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "quantity" => 4, "size" => "PX"}] + "updates" => [{"type" => "web", "quantity" => 4, "size" => "PX"}] }.to_json }, :body => [{"quantity" => 4, "size" => "PX", "type" => "web"}], @@ -364,11 +364,12 @@ describe "ps:resize" do it "can resize using a key/value format" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [{"type" => "web", "size" => "1X", "quantity" => 1}], status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { - "updates" => [{"process" => "web", "size" => "2X"}] + "updates" => [{"type" => "web", "size" => "2X", "quantity" => 1}] }.to_json }, :body => [{"quantity" => 2, "size" => "2X", "type" => "web"}], @@ -377,19 +378,25 @@ stderr, stdout = execute("ps:resize web=2X") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Resizing and restarting the specified dynos... done -web dynos now 2X ($72/month) +dyno type qty cost/mo +---- ---- --- ------- +web 1X 1 36 STDOUT end it "can resize multiple types in one call" do + formation = [ + {"type" => "web", "size" => "1X", "quantity" => 1}, + {"type" => "worker", "size" => "1X", "quantity" => 1}, + ] + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: formation, status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "size" => "4x"}, - {"process" => "worker", "size" => "2X"} + {"type" => "web", "size" => "4x", "quantity" => 1}, + {"type" => "worker", "size" => "2X", "quantity" => 1} ] }.to_json }, @@ -402,20 +409,26 @@ stderr, stdout = execute("ps:resize web=4x worker=2X") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Resizing and restarting the specified dynos... done -web dynos now 1X ($36/month) -worker dynos now 2X ($72/month) +dyno type qty cost/mo +------ ---- --- ------- +web 1X 1 36 +worker 1X 1 36 STDOUT end - it "accepts P as a valid size, with a price of $0.80/hour" do + it "accepts PX as a valid size, with a price of $0.80/hour" do + formation = [ + {"type" => "web", "size" => "1X", "quantity" => 1}, + {"type" => "worker", "size" => "1X", "quantity" => 1}, + ] + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: formation, status: 200}) Excon.stub( { :method => :patch, :path => "/apps/example/formation", :body => { "updates" => [ - {"process" => "web", "size" => "PX"}, - {"process" => "worker", "size" => "Px"} + {"type" => "web", "size" => "PX", "quantity" => 1}, + {"type" => "worker", "size" => "Px", "quantity" => 1} ] }.to_json }, @@ -428,9 +441,10 @@ stderr, stdout = execute("ps:resize web=PX worker=Px") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Resizing and restarting the specified dynos... done -web dynos now PX ($576/month) -worker dynos now PX ($576/month) +dyno type qty cost/mo +------ ---- --- ------- +web 1X 1 36 +worker 1X 1 36 STDOUT end From 09ea29eb52127ad82d4af139fe917edc73782236 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 15 Jun 2015 13:55:31 -0700 Subject: [PATCH 596/952] v3.38.1 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cb9da9fe0..0603d2453 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.38.1 2015-06-15 +================= +Fix ps:type for new process types +Made ps:type output consistent + 3.38.0 2015-06-15 ================= Fix ps:scale for new process types diff --git a/Gemfile.lock b/Gemfile.lock index 6b2a66f27..1a711babd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.38.0) + heroku (3.38.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4fccc5921..fc650d51f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.38.0" + VERSION = "3.38.1" end From 5d433e5a9ecf2fc1853cbebbceac739012c8840d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 16 Jun 2015 15:34:48 -0700 Subject: [PATCH 597/952] made addons help more clear --- lib/heroku/command/addons.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 295e44584..4d2a8b887 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -89,7 +89,7 @@ def plans display_table(plans, %w[default name human_name price], [nil, 'Slug', 'Name', 'Price']) end - # addons:create {SERVICE,PLAN} + # addons:create SERVICE:PLAN # # create an add-on resource # From 5444439f15ac2ab5d3ff06f087384cdb32af383e Mon Sep 17 00:00:00 2001 From: tef Date: Wed, 17 Jun 2015 11:08:26 -0700 Subject: [PATCH 598/952] Allow user to set --wait-interval --- lib/heroku/command/pg_backups.rb | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 971f1bbbf..3bec2f16d 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -6,14 +6,20 @@ class Heroku::Command::Pg < Heroku::Command::Base # pg:copy SOURCE TARGET # + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) + # # Copy all data from source database to target. At least one of # these must be a Heroku Postgres database. + # def copy source_db = shift_argument target_db = shift_argument validate_arguments! + interval = options[:wait_interval].to_i || 3 + interval = [3, interval].max + source = resolve_db_or_url(source_db) target = resolve_db_or_url(target_db) @@ -41,7 +47,7 @@ def copy if confirm_command(confirm_with, message) xfer = hpg_client(attachment).pg_copy(source.name, source.url, target.name, target.url) - poll_transfer('copy', xfer[:uuid]) + poll_transfer('copy', xfer[:uuid], interval) end end @@ -52,7 +58,9 @@ def copy # # info BACKUP_ID # get information about a specific backup # capture DATABASE # capture a new backup + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) # restore [[BACKUP_ID] DATABASE] # restore a backup (default latest) to a database (default DATABASE_URL) + # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) # public-url BACKUP_ID # get secret but publicly accessible URL for BACKUP_ID to download it # -q, --quiet # Hide expiration message (for use in scripts) # cancel [BACKUP_ID] # cancel an in-progress backup or restore (default newest) @@ -346,6 +354,10 @@ def capture_backup attachment = generate_resolver.resolve(db, "DATABASE_URL") validate_arguments! + interval = options[:wait_interval].to_i || 3 + interval = [3, interval].max + + backup = hpg_client(attachment).backups_capture display <<-EOF Use Ctrl-C at any time to stop monitoring progress; the backup @@ -355,7 +367,7 @@ def capture_backup #{attachment.name} ---backup---> #{transfer_name(backup)} EOF - poll_transfer('backup', backup[:uuid]) + poll_transfer('backup', backup[:uuid], interval) end def restore_backup @@ -373,6 +385,8 @@ def restore_backup attachment = generate_resolver.resolve(db, "DATABASE_URL") validate_arguments! + interval = options[:wait_interval].to_i || 3 + interval = [3, interval].max restore_url = nil if restore_from =~ %r{\Ahttps?://} @@ -412,11 +426,11 @@ def restore_backup #{transfer_name(restore)} ---restore---> #{attachment.name} EOF - poll_transfer('restore', restore[:uuid]) + poll_transfer('restore', restore[:uuid], interval) end end - def poll_transfer(action, transfer_id) + def poll_transfer(action, transfer_id, interval) # pending, running, complete--poll endpoint to get backup = nil ticks = 0 @@ -439,7 +453,7 @@ def poll_transfer(action, transfer_id) raise end end - sleep 3 + sleep interval end until backup[:finished_at] if backup[:succeeded] redisplay "#{action.capitalize} completed\n" From 1e2384b44a934dabc5cd2ebf27ff48e0f3c5801d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 18 Jun 2015 13:22:53 -0700 Subject: [PATCH 599/952] fix bug with hidden size this fixes commands like `heroku ps:scale worker=1 web=1:free` --- lib/heroku/command/ps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index db489e38b..fc73820bc 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -381,7 +381,7 @@ def change_tier_if_needed(formation, changes) # some of the proposed changes are in a different tier than existing dyno types # this is not allowed from_type = formation[0]["size"] - to_type = changes[0]["size"] + to_type = changes.map{|c| c["size"]}.compact.first error("Cannot mix #{to_type} with #{from_type} dynos.\nTo change all dynos to #{to_type}, run `heroku ps:type #{to_type}`.") end end From b7871b7651950c47c6571e969f9a3f0b049a9a78 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 18 Jun 2015 18:46:42 -0700 Subject: [PATCH 600/952] fix default v4 commands --- lib/heroku/jsplugin.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 269965fce..ea7ea741e 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -8,11 +8,11 @@ def self.setup? end def self.try_takeover(command, args) - command = command.split(':') - if command.length == 1 - command = commands.find { |t| t["topic"] == command[0] && t["command"] == nil } + topic, cmd = command.split(':') + if cmd + command = commands.find { |t| t["topic"] == topic && t["command"] == cmd } else - command = commands.find { |t| t["topic"] == command[0] && t["command"] == command[1] } + command = commands.find { |t| t["topic"] == topic && (t["command"] == nil || t["default"]) } end return if !command || command["hidden"] run(command['topic'], command['command'], ARGV[1..-1]) From 33a907fba61c7f6b566bcc2fa98445a27c198eee Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 19 Jun 2015 12:47:29 -0700 Subject: [PATCH 601/952] v3.38.2 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0603d2453..f2abbe01c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.38.2 2015-06-19 +================= +Added --wait-interval to pgbackups commands +Fixed minor display bug with ps:scale +Added default flag for v4 commands + 3.38.1 2015-06-15 ================= Fix ps:type for new process types diff --git a/Gemfile.lock b/Gemfile.lock index 1a711babd..deff2dc70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.38.1) + heroku (3.38.2) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index fc650d51f..270e86e88 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.38.1" + VERSION = "3.38.2" end From bec3c5b5e8aa886739197d7c3f156db6c1f7ffb8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 19 Jun 2015 14:23:07 -0700 Subject: [PATCH 602/952] added HEROKU_HEADERS to append extra headers --- lib/heroku/auth.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 9c1e795ee..85f8f5d69 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -17,6 +17,11 @@ def api api = Heroku::API.new(default_params.merge(:api_key => password)) def api.request(params, &block) + if ENV['HEROKU_HEADERS'] + headers = JSON.parse(ENV['HEROKU_HEADERS']) + params[:headers] ||= {} + params[:headers].merge!(headers) + end response = super if response.headers.has_key?('X-Heroku-Warning') Heroku::Command.warnings.concat(response.headers['X-Heroku-Warning'].split("\n")) From da12ceabf7196b8da70dfe678aaca71683043a2f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 19 Jun 2015 14:25:10 -0700 Subject: [PATCH 603/952] v3.38.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f2abbe01c..8c6800225 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.38.3 2015-06-19 +================= +Added HEROKU_HEADERS to append extra request headers + 3.38.2 2015-06-19 ================= Added --wait-interval to pgbackups commands diff --git a/Gemfile.lock b/Gemfile.lock index deff2dc70..27a2ac2b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.38.2) + heroku (3.38.3) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 270e86e88..ebc1ac19d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.38.2" + VERSION = "3.38.3" end From 9cb3ba35cb5bd510a03e5fdd236272a1723697bc Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 19 Jun 2015 19:40:17 -0700 Subject: [PATCH 604/952] merge extra headers for restclient --- lib/heroku/client.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 81e1871f5..013134679 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -643,12 +643,16 @@ def extract_warning(response) end def heroku_headers # :nodoc: - { + headers = { 'X-Heroku-API-Version' => '2', 'User-Agent' => Heroku.user_agent, 'X-Ruby-Version' => RUBY_VERSION, 'X-Ruby-Platform' => RUBY_PLATFORM } + if ENV['HEROKU_HEADERS'] + headers.merge! json_decode(ENV['HEROKU_HEADERS']) + end + headers end def xml(raw) # :nodoc: From db0eee824679461044c8f4225964b7c78d6b9578 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sun, 21 Jun 2015 07:35:36 -0700 Subject: [PATCH 605/952] move twofactor commands to v4 --- lib/heroku/command/two_factor.rb | 46 +++++--------------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb index c9c94f56d..fcbb6bceb 100644 --- a/lib/heroku/command/two_factor.rb +++ b/lib/heroku/command/two_factor.rb @@ -9,17 +9,8 @@ class TwoFactor < BaseWithApp # Display whether two-factor authentication is enabled or not # def index - account = api.request( - :expects => 200, - :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, - :method => :get, - :path => "/account").body - - if account["two_factor_authentication"] - display "Two-factor authentication is enabled." - else - display "Two-factor authentication is not enabled." - end + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('twofactor', nil, ARGV[1..-1]) end alias_command "2fa", "twofactor" @@ -29,22 +20,8 @@ def index # Disable two-factor authentication for your account # def disable - print "Password (typing will be hidden): " - password = Heroku::Auth.ask_for_password - - update = MultiJson.dump( - :two_factor_authentication => false, - :password => password) - - api.request( - :expects => 200, - :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, - :method => :patch, - :path => "/account", - :body => update) - display "Disabled two-factor authentication." - rescue Heroku::API::Errors::RequestFailed => e - error Heroku::Command.extract_error(e.response.body) + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('twofactor', 'disable', ARGV[1..-1]) end alias_command "2fa:disable", "twofactor:disable" @@ -55,19 +32,8 @@ def disable # Generates and replaces recovery codes # def generate_recovery_codes - code = Heroku::Auth.ask_for_second_factor - - recovery_codes = api.request( - :expects => 200, - :method => :post, - :path => "/account/two-factor/recovery-codes", - :headers => { "Heroku-Two-Factor-Code" => code } - ).body - - display "Recovery codes:" - recovery_codes.each { |c| display c } - rescue RestClient::Unauthorized => e - error Heroku::Command.extract_error(e.http_body) + Heroku::JSPlugin.setup + Heroku::JSPlugin.run('twofactor', 'generate-recovery-codes', ARGV[1..-1]) end alias_command "2fa:generate-recovery-codes", "twofactor:generate_recovery_codes" From 17f1f41688aabba2dd68a7188b9de5365d8b2028 Mon Sep 17 00:00:00 2001 From: Yannick Date: Wed, 24 Jun 2015 16:19:05 +0200 Subject: [PATCH 606/952] Only split the command by one ':' to separate topic from command. This is useful for nested commands. ``` 'a:b:c' => { topic: 'a', command: 'b:c' } ``` This seems to be the behavior we get everywhere else. Maybe not a lot of commands are nested but we start using a second level of nesting. --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index ea7ea741e..c5d797be0 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -8,7 +8,7 @@ def self.setup? end def self.try_takeover(command, args) - topic, cmd = command.split(':') + topic, cmd = command.split(':', 1) if cmd command = commands.find { |t| t["topic"] == topic && t["command"] == cmd } else From 748d85c36e38da8d451e21084fe62edd213fb220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Wed, 24 Jun 2015 14:30:12 -0700 Subject: [PATCH 607/952] Add pg:links command to pg commands. --- lib/heroku/client/heroku_postgresql.rb | 13 ++++ lib/heroku/command/pg.rb | 87 +++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index c812351e7..16a98ae11 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -98,6 +98,19 @@ def maintenance_window_set(description) http_put "#{resource_name}/maintenance_window", 'description' => description end + # links + def link_list + http_get "#{resource_name}/links" + end + + def link_set(target, as = nil) + http_post "#{resource_name}/links", 'target' => target, 'as' => as + end + + def link_delete(id) + http_delete "#{resource_name}/links/#{id}" + end + # backups def backups http_get "#{resource_name}/transfers" diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 37ce69ea1..b52223252 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -488,9 +488,94 @@ def upgrade end end + # pg:links + # + # Create links between data stores. Without a subcommand, it lists all + # databases and information on the link. + # + # create # Create a data link + # --as # override the default link name + # destroy # Destroy a data link between a local and remote database + # + def links + mode = shift_argument || 'list' + + if !(%w(list create destroy).include?(mode)) + Heroku::Command.run(current_command, ["--help"]) + exit(1) + end + + case mode + when 'list' + db = shift_argument + resolver = generate_resolver + + if db + dbs = [resolver.resolve(db, "DATABASE_URL")] + else + dbs = resolver.all_databases.values + end + + error("No database attached to this app.") if dbs.compact.empty? + + dbs.each_with_index do |attachment, index| + response = hpg_client(attachment).link_list + display "\n" if index.nonzero? + + styled_header("#{attachment.display_name} (#{attachment.resource_name})") + + next display response[:message] if response.kind_of?(Hash) + next display "No data sources are linked into this database." if response.empty? + response.each do |link| + display "==== #{link[:name]}" + + link[:created] = time_format(link[:created_at]) + link[:remote] = "#{link[:remote]['attachment_name']} (#{link[:remote]['name']})" + link.reject! { |k,_| [:id, :created_at, :name].include?(k) } + styled_hash(Hash[link.map {|k, v| [humanize(k), v] }]) + end + end + when 'create' + remote = shift_argument + local = shift_argument + + error("Usage links ") unless [local, remote].all? + + local_attachment = generate_resolver.resolve(local, "DATABASE_URL") + remote_attachment = resolve_db_or_url(remote) + + output_with_bang("No source database specified.") unless local_attachment + output_with_bang("No remote database specified.") unless remote_attachment + + response = hpg_client(local_attachment).link_set(remote_attachment.url, options[:as]) + + display("New link '#{response[:name]}' successfully created.") + when 'destroy' + local = shift_argument + link = shift_argument + + error("No local database specified.") unless local + error("No link name specified.") unless link + + local_attachment = generate_resolver.resolve(local, "DATABASE_URL") + + message = [ + "WARNING: Destructive Action", + "This command will affect the database: #{local}", + "This will delete #{link} along with the tables and views created within it.", + "This may have adverse effects for software written against the #{link} schema." + ].join("\n") + + if confirm_command(app, message) + action("Deleting link #{link} in #{local}") do + hpg_client(local_attachment).link_delete(link) + end + end + end + end -private + private def resolve_heroku_url(remote) generate_resolver.resolve(remote).url From bb542781793144de2218367405e4c170bfd6e90a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Wed, 24 Jun 2015 16:07:45 -0700 Subject: [PATCH 608/952] v3.39.0 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8c6800225..f0c731184 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.39.0 2015-06-24 +================= +Add pg:links command + 3.38.3 2015-06-19 ================= Added HEROKU_HEADERS to append extra request headers diff --git a/Gemfile.lock b/Gemfile.lock index 27a2ac2b8..5700ef019 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.38.3) + heroku (3.39.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index ebc1ac19d..9c35f82b3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.38.3" + VERSION = "3.39.0" end From 37a651c4e19b43e04581013660ebc75ea8747407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Fri, 26 Jun 2015 09:33:28 -0700 Subject: [PATCH 609/952] Use a proper resolver for pg:links. --- lib/heroku/command/pg.rb | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index b52223252..72d56f85f 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -543,7 +543,7 @@ def links error("Usage links ") unless [local, remote].all? local_attachment = generate_resolver.resolve(local, "DATABASE_URL") - remote_attachment = resolve_db_or_url(remote) + remote_attachment = resolve_service_or_url(remote) output_with_bang("No source database specified.") unless local_attachment output_with_bang("No remote database specified.") unless remote_attachment @@ -577,6 +577,28 @@ def links private + def resolve_service_or_url(name_or_url, default=nil) + if name_or_url =~ %r{(postgres://|redis://)} + url = name_or_url + uri = URI.parse(url) + name = url_name(uri) + MaybeAttachment.new(name, url, nil) + else + attachment_name = name_or_url || default + attachment = (resolve_addon(attachment_name) || []).first + + error("Remote database could not be found.") unless attachment + error("Remote database is invalid.") unless attachment['addon_service']['name'] =~ /heroku-(redis|postgresql)/ + + MaybeAttachment.new(attachment_name, get_config_var(attachment['config_vars'].first), attachment) + end + end + + def get_config_var(name) + res = api.get_config_vars(app) + res.data[:body][name] + end + def resolve_heroku_url(remote) generate_resolver.resolve(remote).url end From 71e17b4a18e0dbfba0a71b1777df92fa4b5a9948 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Tue, 26 May 2015 16:54:03 -0700 Subject: [PATCH 610/952] Split domains command into two tables for Development Domain and Custom Domains with new CNAME Target column --- lib/heroku/api/domains_v3_domain_cname.rb | 29 ++++++++ lib/heroku/command/domains.rb | 46 +++++++++--- spec/heroku/command/domains_spec.rb | 86 +++++++++++++++++++++-- 3 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 lib/heroku/api/domains_v3_domain_cname.rb diff --git a/lib/heroku/api/domains_v3_domain_cname.rb b/lib/heroku/api/domains_v3_domain_cname.rb new file mode 100644 index 000000000..141dab356 --- /dev/null +++ b/lib/heroku/api/domains_v3_domain_cname.rb @@ -0,0 +1,29 @@ +module Heroku + class API + # TODO: rename methods and filename after 3.domain-cname is merged + + def get_domains_v3_domain_cname(app) + request( + :expects => 200, + :method => :get, + :path => "/apps/#{app}/domains", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.domain-cname" + } + ) + end + + def post_domains_v3_domain_cname(app, hostname) + request( + :expects => 201, + :method => :post, + :path => "/apps/#{app}/domains", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.domain-cname", + "Content-Type" => "application/json" + }, + body: Heroku::Helpers.json_encode({'hostname' => hostname}) + ) + end + end +end diff --git a/lib/heroku/command/domains.rb b/lib/heroku/command/domains.rb index 00ef0dac2..5364fdbea 100644 --- a/lib/heroku/command/domains.rb +++ b/lib/heroku/command/domains.rb @@ -1,29 +1,48 @@ require "heroku/command/base" +require "heroku/api/domains_v3_domain_cname" module Heroku::Command - # manage custom domains + # manage domains # class Domains < Base # domains # - # list custom domains for an app + # list domains for an app # #Examples: # # $ heroku domains - # === Domain names for example - # example.com + # === example Heroku Domain + # example.herokuapp.com + # + # === example Custom Domains + # Domain Name DNS Target + # ----------- --------------------- + # example.com example.herokuapp.com # def index validate_arguments! - domains = api.get_domains(app).body - if domains.length > 0 - styled_header("#{app} Domain Names") - styled_array domains.map {|domain| domain["domain"]} + domains = api.get_domains_v3_domain_cname(app).body + + styled_header("#{app} Heroku Domain") + heroku_domain = domains.detect { |d| d['kind'] == 'heroku' || d['kind'] == 'default' } # TODO: remove 'default' after API change + if heroku_domain + display heroku_domain['hostname'] else - display("#{app} has no domain names.") + output_with_bang "Not found" + end + + display + + styled_header("#{app} Custom Domains") + custom_domains = domains.select{ |d| d['kind'] == 'custom' } + if custom_domains.length > 0 + display_table(custom_domains, ['hostname', 'cname'], ['Domain Name', 'DNS Target']) + else + display("#{app} has no custom domains.") + display("Use `heroku domains:add DOMAIN` to add one.") end end @@ -36,14 +55,19 @@ def index # $ heroku domains:add example.com # Adding example.com to example... done # + # ! Configure your app's DNS provider to point to the DNS Target example.herokuapp.com + # ! For help with custom domains, see https://devcenter.heroku.com/articles/custom-domains + # def add unless domain = shift_argument error("Usage: heroku domains:add DOMAIN\nMust specify DOMAIN to add.") end validate_arguments! - action("Adding #{domain} to #{app}") do - api.post_domain(app, domain) + domain = action("Adding #{domain} to #{app}") do + api.post_domains_v3_domain_cname(app, domain).body end + output_with_bang "Configure your app's DNS provider to point to the DNS Target #{domain['cname']}" + output_with_bang "For help, see https://devcenter.heroku.com/articles/custom-domains" end # domains:remove DOMAIN diff --git a/spec/heroku/command/domains_spec.rb b/spec/heroku/command/domains_spec.rb index c19f78fb2..734ec0e71 100644 --- a/spec/heroku/command/domains_spec.rb +++ b/spec/heroku/command/domains_spec.rb @@ -17,37 +17,109 @@ module Heroku::Command stub_core end + # TODO: rename after 3.domain-cname is merged + def stub_get_domains_v3_domain_cname(*custom_hostnames) + Excon.stub( + :headers => { "Accept" => "application/vnd.heroku+json; version=3.domain-cname" }, + :method => :get, + :path => '/apps/example/domains') do + { + :body => ( + [ + { + 'kind' => 'heroku', + 'hostname' => 'example.herokuapp.com', + 'cname' => nil + } + ] + custom_hostnames.map { |hostname| + { 'kind' => 'custom', + 'hostname' => hostname, + 'cname' => 'example-2121.herokussl.com' + } + } + ).to_json, + } + end + end + + # TODO: rename after 3.domain-cname is merged + def stub_post_domains_v3_domain_cname(custom_hostname) + Excon.stub( + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.domain-cname", + "Content-Type" => "application/json" + }, + :method => :post, + :path => '/apps/example/domains') do + { + :status => 201, + :body => { + 'kind' => 'custom', + 'hostname' => custom_hostname, + 'cname' => 'example-2121.herokussl.com' + }.to_json, + } + end + end + context("index") do it "lists message with no domains" do + Excon.stub(:path => '/apps/example/domains') {{ :body => [].to_json }} + stderr, stdout = execute("domains") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -example has no domain names. +=== example Heroku Domain + ! Not found + +=== example Custom Domains +example has no custom domains. +Use `heroku domains:add DOMAIN` to add one. +STDOUT + end + + it "lists message with development domain but no custom domains" do + stub_get_domains_v3_domain_cname() + stderr, stdout = execute("domains") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Heroku Domain +example.herokuapp.com + +=== example Custom Domains +example has no custom domains. +Use `heroku domains:add DOMAIN` to add one. STDOUT end - it "lists domains when some exist" do - api.post_domain("example", "example.com") + it "lists development and custom domains when some exist" do + stub_get_domains_v3_domain_cname('example1.com', 'example2.com') stderr, stdout = execute("domains") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -=== example Domain Names -example.com +=== example Heroku Domain +example.herokuapp.com +=== example Custom Domains +Domain Name DNS Target +------------ -------------------------- +example1.com example-2121.herokussl.com +example2.com example-2121.herokussl.com STDOUT - api.delete_domain("example", "example.com") end end it "adds domain names" do + stub_post_domains_v3_domain_cname('example.com') stderr, stdout = execute("domains:add example.com") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT Adding example.com to example... done + ! Configure your app's DNS provider to point to the DNS Target example-2121.herokussl.com + ! For help, see https://devcenter.heroku.com/articles/custom-domains STDOUT - api.delete_domain("example", "example.com") end it "shows usage if no domain specified for add" do From ed27b5b6a565522cad6e1f5829c3a3b48397b55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Fri, 26 Jun 2015 15:25:34 -0700 Subject: [PATCH 611/952] v3.39.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f0c731184..1641f94ef 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.39.1 2015-06-26 +================= +Fix pg:links remote resolver + 3.39.0 2015-06-24 ================= Add pg:links command diff --git a/Gemfile.lock b/Gemfile.lock index 5700ef019..3741f9fb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.39.0) + heroku (3.39.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9c35f82b3..1461acd88 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.39.0" + VERSION = "3.39.1" end From b74e17abdf5dbcd09064b0e74b46cdd5dbe420f2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 29 Jun 2015 16:25:32 -0700 Subject: [PATCH 612/952] fix ps scaling pricing display for unknown types --- lib/heroku/command/ps.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index fc73820bc..29f93ec55 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -321,12 +321,12 @@ def patch_tier(process_tier) def display_dyno_type_and_costs(formation) annotated = formation.sort_by{|d| d['type']}.map do |dyno| - cost = COSTS[dyno["size"]] * dyno["quantity"] + cost = COSTS[dyno["size"]] { 'dyno' => dyno['type'], 'type' => dyno['size'].rjust(4), 'qty' => dyno['quantity'].to_s.rjust(3), - 'cost/mo' => cost.to_s.rjust(7) + 'cost/mo' => cost ? (cost * dyno["quantity"]).to_s.rjust(7) : '' } end From 3b4a1d6c8aeae546a228a2707d71f7b65421868f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 30 Jun 2015 15:52:14 -0700 Subject: [PATCH 613/952] removed heroku-redis shim --- lib/heroku/command/redis.rb | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 lib/heroku/command/redis.rb diff --git a/lib/heroku/command/redis.rb b/lib/heroku/command/redis.rb deleted file mode 100644 index 390ba74e5..000000000 --- a/lib/heroku/command/redis.rb +++ /dev/null @@ -1,19 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # list redis databases for an app - # - class Redis < Base - - # redis [DATABASE] - # - # Get information about redis database - # - # - def index - Heroku::JSPlugin.install('heroku-redis') - Heroku::JSPlugin.run('redis:info', nil, ARGV[1..-1]) - end - end -end From c3cb86f53cb98336940e8b3d421f737a72a6980c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 3 Jul 2015 06:46:40 -0700 Subject: [PATCH 614/952] error if cant install v4 plugin --- lib/heroku/jsplugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index c5d797be0..ba13c4b11 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -87,6 +87,7 @@ def self.commands_info def self.install(name, opts={}) self.setup system "\"#{bin}\" plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) + error "error installing plugin #{name}" if $? != 0 end def self.uninstall(name) From 59023df8cc735db62f7aa20fb3419d9cd8e837d7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 3 Jul 2015 12:34:36 -0700 Subject: [PATCH 615/952] v3.39.2 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1641f94ef..96017a970 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.39.2 2015-07-03 +================= +Error out if v4 plugin fails to install +Updated domains command +Fix v4 commands with ':' in command name +Fixed ps pricing for unknown types + 3.39.1 2015-06-26 ================= Fix pg:links remote resolver diff --git a/Gemfile.lock b/Gemfile.lock index 3741f9fb2..6a1f9359b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.39.1) + heroku (3.39.2) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1461acd88..52b9593b3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.39.1" + VERSION = "3.39.2" end From b5921443449781239ed8567461d82a04b8025340 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Mon, 6 Jul 2015 15:49:18 +1000 Subject: [PATCH 616/952] Group attachments by resource in pg:info Bonus: this likely makes fewer requests for multiple attachments. --- lib/heroku/command/pg.rb | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 72d56f85f..ba88e2ad6 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -647,23 +647,22 @@ def hpg_databases_with_info return @hpg_databases_with_info if @hpg_databases_with_info @resolver = generate_resolver - dbs = @resolver.all_databases + attachments = @resolver.all_databases - has_promoted = dbs.any? { |_, att| att.primary_attachment? } - unique_dbs = dbs.reject { |var, _| has_promoted && 'DATABASE_URL' == var }.map{|_, att| att}.compact + attachments_by_db = attachments.values.group_by(&:resource_name) db_infos = {} mutex = Mutex.new - threads = (0..unique_dbs.size-1).map do |i| + threads = attachments_by_db.map do |resource, attachments| Thread.new do - att = unique_dbs[i] + name = attachments.map(&:config_var).sort.join(', ') begin - info = hpg_info(att, options[:extended]) + info = hpg_info(attachments.first, options[:extended]) rescue info = nil end mutex.synchronize do - db_infos[att.display_name] = info + db_infos[name] = info end end end @@ -674,7 +673,13 @@ def hpg_databases_with_info end def hpg_info(attachment, extended=false) - hpg_client(attachment).get_database(extended) + info = hpg_client(attachment).get_database(extended) + + # TODO: Make this the section title and list the current `name` as an + # "Attachments" item here: + info[:info] << {"name" => "Add-on", "values" => [attachment.resource_name]} + + info end def hpg_info_display(item) From 35414f46f620100080cbeeb3bc43a8277fd68b94 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Mon, 6 Jul 2015 15:53:15 +1000 Subject: [PATCH 617/952] Don't mutate (corrupts test data) --- lib/heroku/command/pg.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index ba88e2ad6..781db603e 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -677,9 +677,7 @@ def hpg_info(attachment, extended=false) # TODO: Make this the section title and list the current `name` as an # "Attachments" item here: - info[:info] << {"name" => "Add-on", "values" => [attachment.resource_name]} - - info + info.merge(:info => info[:info] + [{"name" => "Add-on", "values" => [attachment.resource_name]}]) end def hpg_info_display(item) From c3f27bf6f196e25f9d79e8591a687a5fde47f6ae Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Mon, 6 Jul 2015 16:10:13 +1000 Subject: [PATCH 618/952] Put DATABASE_URL last in pg:info output --- lib/heroku/command/pg.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 781db603e..352c1127f 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -655,12 +655,20 @@ def hpg_databases_with_info mutex = Mutex.new threads = attachments_by_db.map do |resource, attachments| Thread.new do - name = attachments.map(&:config_var).sort.join(', ') begin info = hpg_info(attachments.first, options[:extended]) rescue info = nil end + + # Make headers as per heroku/heroku#1605 + names = attachments.map(&:config_var) + names << 'DATABASE_URL' if attachments.any? { |att| att.primary_attachment? } + name = names. + uniq. + sort_by { |n| n=='DATABASE_URL' ? '{' : n }. # Weight DATABASE_URL last + join(', ') + mutex.synchronize do db_infos[name] = info end From 5b3e4f7bd7d72820a2e14ab20e8071eeba35210d Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Mon, 6 Jul 2015 16:11:12 +1000 Subject: [PATCH 619/952] Fix specs --- spec/heroku/command/pg_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 23009b882..847760d86 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -94,8 +94,9 @@ module Heroku::Command Fork/Follow: Available Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: whatever-something-2323 -=== HEROKU_POSTGRESQL_IVORY_URL (DATABASE_URL) +=== HEROKU_POSTGRESQL_IVORY_URL, DATABASE_URL Plan: Ronin Status: available Data Size: 1 MB @@ -104,6 +105,7 @@ module Heroku::Command Fork/Follow: Available Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: loudly-yelling-1232 === HEROKU_POSTGRESQL_RONIN_URL Plan: Ronin @@ -114,6 +116,7 @@ module Heroku::Command Fork/Follow: Available Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: softly-mocking-123 STDOUT end @@ -146,6 +149,7 @@ module Heroku::Command Forked From: Database on postgreshost.com:5432/database_name Created: 2011-12-13 00:00 UTC Maintenance: not required +Add-on: softly-mocking-123 STDOUT end From 4c171dc5c7a86b104e4d31c76dc21dc0d8ada9c8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 6 Jul 2015 11:27:43 -0700 Subject: [PATCH 620/952] v3.39.3 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6a1f9359b..84a65b61a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.39.2) + heroku (3.39.3) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 52b9593b3..fdb2040d3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.39.2" + VERSION = "3.39.3" end From a359dbadfc16d2509ef52e038bb4d2bea9d40ff2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 6 Jul 2015 11:30:49 -0700 Subject: [PATCH 621/952] fixed bug with v4 command name splitting --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index ba13c4b11..afcf1fbbe 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -8,7 +8,7 @@ def self.setup? end def self.try_takeover(command, args) - topic, cmd = command.split(':', 1) + topic, cmd = command.split(':', 2) if cmd command = commands.find { |t| t["topic"] == topic && t["command"] == cmd } else From 6f96aca2279d39930790d029393b4c447e8835aa Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 6 Jul 2015 11:31:29 -0700 Subject: [PATCH 622/952] v3.39.4 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 96017a970..ef77b1b2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.39.4 2015-07-06 +================= +Fixed bug with v4 command name splitting + 3.39.2 2015-07-03 ================= Error out if v4 plugin fails to install diff --git a/Gemfile.lock b/Gemfile.lock index 84a65b61a..fb852645f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.39.3) + heroku (3.39.4) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index fdb2040d3..49ddfff58 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.39.3" + VERSION = "3.39.4" end From e9a32ffef52e9bafb82d8f53446f720a5dac6d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Tue, 7 Jul 2015 10:30:21 -0700 Subject: [PATCH 623/952] Add missing method. --- lib/heroku/command/pg.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 72d56f85f..82c52dc70 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -639,6 +639,10 @@ def in_maintenance?(app) api.get_app_maintenance(app).body['maintenance'] end + def time_format(time) + Time.parse(time).getutc.strftime("%Y-%m-%d %H:%M %Z") + end + def hpg_client(attachment) Heroku::Client::HerokuPostgresql.new(attachment) end From 2c9a93254955e70bec0e5176da5054316d9ad01e Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 9 Jul 2015 18:50:18 -0700 Subject: [PATCH 624/952] fix pg for sudo commands --- lib/heroku/client/heroku_postgresql.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index 16a98ae11..c28616474 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -37,6 +37,9 @@ def resource_name end def heroku_postgresql_resource + if ENV['HEROKU_HEADERS'] + self.class.add_headers json_decode(ENV['HEROKU_HEADERS']) + end RestClient::Resource.new( "https://#{heroku_postgresql_host}/client/v11/databases", :user => Heroku::Auth.user, From 64a558ef3f3f3aff66c31e6c2ee68b1116279d06 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 10 Jul 2015 10:48:46 -0700 Subject: [PATCH 625/952] updated local help --- lib/heroku/command/local.rb | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/local.rb b/lib/heroku/command/local.rb index bcd7e7dbe..19fb6a9ea 100644 --- a/lib/heroku/command/local.rb +++ b/lib/heroku/command/local.rb @@ -5,23 +5,21 @@ module Heroku::Command # run heroku app locally class Local < Base - # local [PROCESSNAME] + # Usage: heroku local [PROCESSNAME] # - # run heroku app locally + # -f, --procfile PROCFILE # use a different Procfile + # -e, --env ENV # location of env file (defaults to .env) + # -c, --concurrency CONCURRENCY # number of processes to start + # -p, --port PORT # port to listen on + # -r, --restart # restart process if it dies # - # Start the application specified by a Procfile (defaults to ./Procfile) + # Start the application specified by a Procfile (defaults to ./Procfile) # - # Examples: + # Examples: # - # heroku local - # heroku local web - # heroku local -f Procfile.test -e .env.test - # - # -f, --procfile PROCFILE - # -e, --env ENV - # -c, --concurrency CONCURRENCY - # -p, --port PORT - # -r, --r + # heroku local + # heroku local web + # heroku local -f Procfile.test -e .env.test # def index Heroku::JSPlugin.install('heroku-local') From 9a9870c6cbe3a984e96aab1b6e15a56fb747303a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 10 Jul 2015 11:00:41 -0700 Subject: [PATCH 626/952] disable ssl for org api on devclouds --- lib/heroku/client/organizations.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 8048a4082..5fdf093a4 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -11,6 +11,7 @@ def api options = {} key = Heroku::Auth.get_credentials[1] auth = "Basic #{Base64.encode64(':' + key).gsub("\n", '')}" hdrs = headers.merge( {"Authorization" => auth } ) + options[:ssl_verify_peer] = Heroku::Auth.verify_host?(Heroku::Auth.host) @connection = Excon.new(manager_url, options.merge(:headers => hdrs)) end From 4ec4af497111945c71e333bb5fa7eeb6df9519dc Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 10 Jul 2015 11:19:37 -0700 Subject: [PATCH 627/952] v3.39.5 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ef77b1b2c..fb52161f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.39.5 2015-07-10 +================= +Group attachments by resource in pg:info +Added missing method used in pg:links +Fixed pg for sudo commands +Updated help for local +Fixed SSL bug with devclouds and org api + 3.39.4 2015-07-06 ================= Fixed bug with v4 command name splitting diff --git a/Gemfile.lock b/Gemfile.lock index fb852645f..882f4dc15 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.39.4) + heroku (3.39.5) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 49ddfff58..0700161c3 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.39.4" + VERSION = "3.39.5" end From 7d5d600f5a240a3d047d00ba1de95d5270b39870 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Sat, 11 Jul 2015 14:37:48 +0900 Subject: [PATCH 628/952] Add space option to apps:create --- .../api/organizations_apps_v3_dogwood.rb | 15 ++++++++++ lib/heroku/command/apps.rb | 8 ++++- spec/heroku/command/apps_spec.rb | 29 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 lib/heroku/api/organizations_apps_v3_dogwood.rb diff --git a/lib/heroku/api/organizations_apps_v3_dogwood.rb b/lib/heroku/api/organizations_apps_v3_dogwood.rb new file mode 100644 index 000000000..fc5ecf62c --- /dev/null +++ b/lib/heroku/api/organizations_apps_v3_dogwood.rb @@ -0,0 +1,15 @@ +module Heroku + class API + def post_organizations_app_v3_dogwood(params={}) + request( + :method => :post, + :body => Heroku::Helpers.json_encode(params), + :expects => 201, + :path => "/organizations/apps", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.dogwood" + } + ) + end + end +end diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 66c9b022c..9b777a196 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -1,5 +1,6 @@ require "heroku/command/base" require "heroku/command/stack" +require "heroku/api/organizations_apps_v3_dogwood" # manage apps (create, destroy) # @@ -195,6 +196,7 @@ def info # -b, --buildpack BUILDPACK # a buildpack url to use for this app # -n, --no-remote # don't create a git remote # -r, --remote REMOTE # the git remote to create, default "heroku" + # --space SPACE # HIDDEN: the space in which to create the app # -s, --stack STACK # the stack on which to create the app # --region REGION # specify region for this app to run in # -l, --locked # lock the app @@ -232,18 +234,22 @@ def create params = { "name" => name, "region" => options[:region], + "space" => options[:space], "stack" => Heroku::Command::Stack::Codex.in(options[:stack]), "locked" => options[:locked] } info = if org org_api.post_app(params, org).body + elsif options[:space] + api.post_organizations_app_v3_dogwood(params).body else api.post_app(params).body end begin - action("Creating #{info['name']}", :org => !!org) do + space_action = info['space'] ? " in space #{info['space']['name']}" : '' + action("Creating #{info['name']}#{space_action}", :org => !!org) do if info['create_status'] == 'creating' Timeout::timeout(options[:timeout].to_i) do loop do diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 8253012df..78f28b1af 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -163,6 +163,35 @@ module Heroku::Command api.delete_app("alternate-remote") end + it "with a space" do + Excon.stub( + :headers => { 'Accept' => 'application/vnd.heroku+json; version=3.dogwood'}, + :method => :post, + :path => '/organizations/apps') do + { + :status => 201, + :body => { + :name => 'spaceapp', + :space => { + :name => 'example-space' + }, + :stack => 'cedar-14', + :web_url => 'http://spaceapp.herokuapp.com/' + }.to_json, + } + end + + with_blank_git_repository do + stderr, stdout = execute("apps:create spaceapp --space example-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Creating spaceapp in space example-space... done, stack is cedar-14 +http://spaceapp.herokuapp.com/ | https://git.heroku.com/spaceapp.git +Git remote heroku added + STDOUT + end + end + end context("index") do From ad59b3dc605a59dd855d77d2ba915d81b7ddeac3 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 15 Jul 2015 18:20:48 +0900 Subject: [PATCH 629/952] Add space attribute to apps:info --- lib/heroku/command/apps.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 66c9b022c..04cdf36f5 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -170,6 +170,7 @@ def info data["Owner Email"] = app_data["owner_email"] if app_data["owner_email"] data["Owner"] = app_data["owner"] if app_data["owner"] data["Region"] = app_data["region"] if app_data["region"] + data["Space"] = app_data["space"]["name"] if app_data["space"] && app_data["space"]["name"] data["Repo Size"] = format_bytes(app_data["repo_size"]) if app_data["repo_size"] data["Slug Size"] = format_bytes(app_data["slug_size"]) if app_data["slug_size"] data["Cache Size"] = format_bytes(app_data["cache_size"]) if app_data["cache_size"] From a4d4cbfa090d06843a94ea2bd82a3cc7ecb19005 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 15 Jul 2015 11:15:15 -0700 Subject: [PATCH 630/952] addons:add should be addons:create --- lib/heroku/command/pgbackups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pgbackups.rb b/lib/heroku/command/pgbackups.rb index 4cd4154bc..2673b0b6d 100644 --- a/lib/heroku/command/pgbackups.rb +++ b/lib/heroku/command/pgbackups.rb @@ -256,7 +256,7 @@ def config_vars def pgbackup_client pgbackups_url = config_vars["PGBACKUPS_URL"] - error("Please add the pgbackups addon first via:\nheroku addons:add pgbackups") unless pgbackups_url + error("Please add the pgbackups addon first via:\nheroku addons:create pgbackups") unless pgbackups_url @pgbackup_client ||= Heroku::Client::Pgbackups.new(pgbackups_url) end From c3668789f5bda09346f8716b70bd88bd65219065 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Fri, 17 Jul 2015 18:45:23 +0900 Subject: [PATCH 631/952] Add hidden option to list apps in a given space --- lib/heroku/command/apps.rb | 24 +++++++++--- spec/heroku/command/apps_spec.rb | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 66c9b022c..6f3a9aeb4 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -9,9 +9,10 @@ class Heroku::Command::Apps < Heroku::Command::Base # # list your apps # - # -o, --org ORG # the org to list the apps for - # -A, --all # list all apps in the org. Not just joined apps - # -p, --personal # list apps in personal account when a default org is set + # -o, --org ORG # the org to list the apps for + # --space SPACE # HIDDEN: list apps in a given space + # -A, --all # list all apps in the org. Not just joined apps + # -p, --personal # list apps in personal account when a default org is set # #Example: # @@ -27,7 +28,11 @@ def index validate_arguments! options[:ignore_no_org] = true - apps = if org + apps = if options[:space] + api.get_apps.body.select do |app| + app["space"] && [app["space"]["name"], app["space"]["id"]].include?(options[:space]) + end + elsif org org_api.get_apps(org).body else api.get_apps.body.select { |app| options[:all] ? true : !org?(app["owner_email"]) } @@ -55,6 +60,9 @@ def index display end end + elsif options[:space] + styled_header("Apps in space #{options[:space]}") + styled_array(apps.map { |app| regionized_app_name(app) }) else my_apps, collaborated_apps = apps.partition { |app| app["owner_email"] == Heroku::Auth.user } @@ -69,7 +77,13 @@ def index end end else - org ? display("There are no apps in organization #{org}.") : display("You have no apps.") + if org then + display("There are no apps in organization #{org}.") + elsif options[:space] + display("There are no apps available in space #{options[:space]}.") + else + display("You have no apps.") + end end end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 8253012df..58e15c697 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -239,6 +239,69 @@ module Heroku::Command end end + context("index with space") do + context("and the space has no apps") do + before(:each) do + @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do + { + :body => MultiJson.dump([]), + :status => 200 + } + end + end + + after(:each) do + Excon.stubs.delete(@space_apps_stub) + end + + it "displays a message when the space has no apps" do + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +There are no apps available in space test-space. +STDOUT + end + end + + context("and the space has apps") do + before(:each) do + @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do + { + :body => MultiJson.dump([ + {"name" => "space-app-1", "space" => {"id" => "test-space-id", "name" => "test-space"}}, + {"name" => "non-space-app-2", "space" => nil} + ]), + :status => 200 + } + end + end + + after(:each) do + Excon.stubs.delete(@space_apps_stub) + end + + it "lists only apps in spaces by name" do + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== Apps in space test-space +space-app-1 + +STDOUT + end + + it "lists only apps in spaces by id" do + stderr, stdout = execute("apps --space test-space-id") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== Apps in space test-space-id +space-app-1 + +STDOUT + end + end + end + context("rename") do context("success") do From 381bf727360c931bcfdc9b01eba00f9c0b0e6c9f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 17 Jul 2015 17:01:59 -0700 Subject: [PATCH 632/952] made release guide more comforting --- RELEASE-FULL.md | 92 +++++++++++++++++++++++++++++++++++++++ RELEASE.md | 112 +++--------------------------------------------- 2 files changed, 98 insertions(+), 106 deletions(-) create mode 100644 RELEASE-FULL.md diff --git a/RELEASE-FULL.md b/RELEASE-FULL.md new file mode 100644 index 000000000..07a5682f7 --- /dev/null +++ b/RELEASE-FULL.md @@ -0,0 +1,92 @@ +Heroku CLI Full Release Process +=============================== + +The following is how to do releases for OSX, Windows, and the "main" release (ubuntu, tgz, zip). If you are not a member of the CLI team and performing a release while they are out, you likely do not need to be concerned with this guide and can use the regular, much easier [release process](./RELEASE.md). + +## OSX Release + +Prerequisites: + +* OSX +* Heroku Developer ID Installer Certificate in Keychain +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` + +To build for testing: `bundle exec rake pkg:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.pkg`. +To release: `bundle exec rake pkg:release`. + +## Windows Release + +This is run not from a Windows machine, but from a UNIX machine with Wine. + +Mac Prerequisites: + +* Heroku Developer ID Installer Certificate in Keychain +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* Install [XQuartz](http://xquartz.macosforge.org/) manually, or via the terminal (restart required): + +```sh +curl -O# http://xquartz-dl.macosforge.org/SL/XQuartz-2.7.6.dmg +hdiutil attach XQuartz-2.7.6.dmg -mountpoint /Volumes/xquartz +sudo installer -store -pkg /Volumes/xquartz/XQuartz.pkg -target / +hdiutil detach /Volumes/xquartz +rm XQuartz-2.7.6.dmg +``` + +* `/opt/X11/bin` should be in your `$PATH` so `Xvfb` can be started. +* Install wine: `brew install wine` +* The pvk file: + +The certificate and private key for code signing are in the repo in: + +> dist/resources/exe/heroku-codesign-cert* + +which is in the format mono signcode wants. + +The pvk file is encrypted. If you want the build not to prompt you for +its passphrase, you'll need to decrypt it. See the `exe:pvk-nocrypt` task. + +Bewake the openssl version on the Mac doesn't work with `exe:pvk-nocrypt`. +See comments on the source code for details and solution. + +If you wanna leave the key encrypted, you still have to link it before +building; run the `exe:pvk` task for that. + +You'll have to ask the right person for the passphrase to the key. + +You then need to initialize a custom wine build environment. The `exe:init-wine` +task will do that for you. + +To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. +To release: `bundle exec rake pkg:release`. + +## Main Release + +This process releases the tgz (standalone/homebrew), zip (for autoupdates), deb package and ruby gem. It's everything that is required to not end up with a partial release. This is what the buildserver does for you, so you shouldn't have to do this manually (this is just for reference). Because this builds a deb package, you must be on an Ubuntu box. + +Prerequisites: + +* Running from Ubuntu +* Make sure you have permissions to `heroku` gem through `gem` https://rubygems.org/gems/heroku. +* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* deb private key +* Ubuntu prerequisites: + +```sh +echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections +sudo apt-get install -y build-essential libpq-dev libsqlite3-dev curl xvfb wine +``` + +If this is your first time, you should first build the packages: `bundle exec rake build` Then look inside `./dist` to test each of the packages. + +Once you are confident it works, release: `bundle exec rake release`. Note that release will automatically build if the packages are not there (there is no need to run `rake build`). + +Note that you can look inside the `Rakefile` to test out each part of the step on your machine before it is built. + +## Ruby versions + +Toolbelt bundles Ruby using different sources according to the OS: + +- Windows: fetches [rubyinstaller.exe](http://rubyinstaller.org/) from S3. +- Mac: fetches ruby.pkg from S3. That file was extracted from +[RailsInstaller](http://railsinstaller.org/en). +- Linux: uses system debs for Ruby. diff --git a/RELEASE.md b/RELEASE.md index 5cafc9e8b..c11243b78 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,7 @@ Heroku CLI Release Process ========================== -Releasing the CLI involves releasing a few different things. The important tasks can all be done on the buildserver. +This is the normal guide on how to do a release. If you are not a member of the CLI team and would like to release a new version of the CLI while they are out, this is the guide you want. ## Releasing with buildserver @@ -14,111 +14,11 @@ Releasing the CLI involves releasing a few different things. The important tasks * Commit the changes `git commit -m "vX.Y.Z" -a` * Push changes to master `git push origin master` * Go to the buildserver and release http://54.148.200.17/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) -* [optional] Release the OSX pkg (instructions below) -* [optional] Release the WIN pkg (instructions below) +* [optional] Release the OSX pkg (instructions in [full release guide](./RELEASE-FULL.md)) +* [optional] Release the WIN pkg (instructions in [full release guide](./RELEASE-FULL.md)) -## Notes +## When should the optional commands be run -The last 2 are optional because existing toolbelts will autoupdate after the first command is run. This isn't the case for deb packages which is why they're included in the main process. There can still be situations (although minor ones) where not releasing the osx/win packages can cause problems so they normally should always be run. +Because the toolbelt autoupdates after the first command is run, not releasing those versions just means the user will be on the previously released version until they run a command, then they'll be on the latest even if OSX and Windows were not released. -The release process will prevent you from releasing an already released version. If you have a bad/incomplete release, you may need to bump the version number again. - -## Main Release - -This process releases the tgz (standalone/homebrew), zip (for autoupdates), deb package and ruby gem. It's everything that is required to not end up with a partial release. This is what the buildserver does for you, so you shouldn't have to do this manually (this is just for reference). Because this builds a deb package, you must be on an Ubuntu box. - -Prerequisites: - -* Running from Ubuntu -* Make sure you have permissions to `heroku` gem through `gem` https://rubygems.org/gems/heroku. -* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` -* deb private key -* Ubuntu prerequisites: - -```sh -echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | sudo debconf-set-selections -sudo apt-get install -y build-essential libpq-dev libsqlite3-dev curl xvfb wine -``` - -If this is your first time, you should first build the packages: `bundle exec rake build` Then look inside `./dist` to test each of the packages. - -Once you are confident it works, release: `bundle exec rake release`. Note that release will automatically build if the packages are not there (there is no need to run `rake build`). - -Note that you can look inside the `Rakefile` to test out each part of the step on your machine before it is built. - -## OSX Release - -Prerequisites: - -* OSX -* Heroku Developer ID Installer Certificate in Keychain -* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` - -To build for testing: `bundle exec rake pkg:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.pkg`. -To release: `bundle exec rake pkg:release`. - -## Windows Release - -This is run not from a Windows machine, but from a UNIX machine with Wine. - -Mac Prerequisites: - -* Heroku Developer ID Installer Certificate in Keychain -* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` -* Install [XQuartz](http://xquartz.macosforge.org/) manually, or via the terminal (restart required): - -```sh -curl -O# http://xquartz-dl.macosforge.org/SL/XQuartz-2.7.6.dmg -hdiutil attach XQuartz-2.7.6.dmg -mountpoint /Volumes/xquartz -sudo installer -store -pkg /Volumes/xquartz/XQuartz.pkg -target / -hdiutil detach /Volumes/xquartz -rm XQuartz-2.7.6.dmg -``` - -* `/opt/X11/bin` should be in your `$PATH` so `Xvfb` can be started. -* Install wine: `brew install wine` -* The pvk file: - -The certificate and private key for code signing are in the repo in: - -> dist/resources/exe/heroku-codesign-cert* - -which is in the format mono signcode wants. - -The pvk file is encrypted. If you want the build not to prompt you for -its passphrase, you'll need to decrypt it. See the `exe:pvk-nocrypt` task. - -Bewake the openssl version on the Mac doesn't work with `exe:pvk-nocrypt`. -See comments on the source code for details and solution. - -If you wanna leave the key encrypted, you still have to link it before -building; run the `exe:pvk` task for that. - -You'll have to ask the right person for the passphrase to the key. - -You then need to initialize a custom wine build environment. The `exe:init-wine` -task will do that for you. - -To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. -To release: `bundle exec rake pkg:release`. - -## Ruby versions - -Toolbelt bundles Ruby using different sources according to the OS: - -- Windows: fetches [rubyinstaller.exe](http://rubyinstaller.org/) from S3. -- Mac: fetches ruby.pkg from S3. That file was extracted from -[RailsInstaller](http://railsinstaller.org/en). -- Linux: uses system debs for Ruby. - -## Changelog (only if there is at least one major new feature) - -* Create a [new changelog](http://devcenter.heroku.com/admin/changelog_items/new) -* Set the title to `Heroku CLI vX.Y.Z released with #{highlights}` -* Set the description to: - - - - A new version of the Heroku CLI is available with #{details}. - - See the [CLI changelog](https://github.com/heroku/heroku/blob/master/CHANGELOG) for details and update by using `heroku update`. +For this reason, you can skip the OSX and Windows steps and probably should if you don't regularly release the CLI. This is because they involve getting a local environment setup to release the CLI and that is easier said than done. From ecf26881534b982f8172f0dc6922e8bc956c5608 Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Mon, 20 Jul 2015 14:25:52 -0700 Subject: [PATCH 633/952] Also allow provider messages on upgrade/downgrade Matches behavior for provisioning --- lib/heroku/command/addons.rb | 8 +++++--- spec/heroku/command/addons_spec.rb | 33 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 4d2a8b887..153062ede 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -256,20 +256,22 @@ def upgrade raise CommandFailed.new("Missing add-on plan") if plan.nil? action("Changing #{addon_name} plan to #{plan}") do - addon = api.request( + @addon = api.request( :body => json_encode({ "plan" => { "name" => plan } }), :expects => 200..300, :headers => { "Accept" => "application/vnd.heroku+json; version=3", - "Accept-Expansion" => "plan" + "Accept-Expansion" => "plan", + "X-Heroku-Legacy-Provider-Messages" => "true" }, :method => :patch, :path => "/apps/#{app}/addons/#{addon_name}" ).body - @status = "(#{format_price addon['plan']['price']})" if addon['plan'].has_key?('price') + @status = "(#{format_price @addon['plan']['price']})" if @addon['plan'].has_key?('price') end + display @addon['provision_message'] unless @addon['provision_message'].to_s.strip == "" end # addons:downgrade ADDON_NAME ADDON_SERVICE:PLAN diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 21cfcd784..f2d15d4e2 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -529,6 +529,39 @@ module Heroku::Command Excon.stubs.shift(2) end + + it "adds an addon with a price and multiline message" do + my_addon = build_addon( + name: "my_addon", + plan: { name: "my_plan" }, + addon_service: { name: "my_service" }, + app: { name: "example" }, + price: { cents: 0, unit: "month" } + ).merge(provision_message: "foo\nbar") + + Excon.stub(method: :get, path: %r(/apps/example/addons)) do + { body: MultiJson.encode([my_addon]), status: 200 } + end + + Excon.stub(method: :patch, path: %r(/apps/example/addons/my_addon)) do + { body: MultiJson.encode(my_addon), status: 200 } + end + + stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "$200/mo", "message" => "foo\nbar" }) + stderr, stdout = execute("addons:upgrade my_service") + expect(stderr).to eq("") + expect(stdout).to eq <<-OUTPUT +WARNING: No add-on name specified (see `heroku help addons:upgrade`) +Finding add-on from service my_service on app example... done +Found my_addon (my_plan) on example. +Changing my_addon plan to my_service... done, (free) +foo +bar +OUTPUT + + Excon.stubs.shift(2) + end + end describe 'downgrading' do From fa4e1058e8bc30e1341133a02fd049d20e314b69 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Tue, 21 Jul 2015 11:39:10 -0700 Subject: [PATCH 634/952] Explicitly block specifying both --space and --org when listing apps --- lib/heroku/command/apps.rb | 4 ++++ spec/heroku/command/apps_spec.rb | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index f9b7ecc2b..cc0c073ab 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -29,6 +29,10 @@ def index validate_arguments! options[:ignore_no_org] = true + if options[:space] && org + error "Specify option for space or org, but not both." + end + apps = if options[:space] api.get_apps.body.select do |app| app["space"] && [app["space"]["name"], app["space"]["id"]].include?(options[:space]) diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index c86b238ec..034572afb 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -331,6 +331,16 @@ module Heroku::Command end end + context("index with space and org") do + it "displays error to not specify both" do + stderr, stdout = execute("apps --space test-space --org test-org") + expect(stdout).to eq("") + expect(stderr).to eq <<-STDERR + ! Specify option for space or org, but not both. +STDERR + end + end + context("rename") do context("success") do From a6e6c81712aae3130588b9f2abe57835adb24fd9 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 21 Jul 2015 12:41:29 -0700 Subject: [PATCH 635/952] Fix issues with backup scheduling Previously, if another config var matches the user-provided alias but its value does *not* match (e.g., the config var for the database the user is trying to schedule is a substring of another config var with a different value), we error out. This fixes that and adds specs around backup scheduling. --- lib/heroku/command/pg_backups.rb | 8 ++-- spec/heroku/command/pg_backups_spec.rb | 57 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 3bec2f16d..13ec527d4 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -562,8 +562,10 @@ def schedule_backups # e.g., names like FOLLOWER_URL work. To do this, we look up the # app config vars and re-find one that looks like the user's # requested name. - db_name, alias_url = resolver.app_config_vars.find { |k,_| k =~ /#{db}/i } - if attachment.url != alias_url + db_name, alias_url = resolver.app_config_vars.find do |k,v| + k =~ /#{db}/i && v == attachment.url + end + if alias_url.nil? error("Could not find database to schedule for backups. Try using its full name.") end @@ -628,7 +630,7 @@ def hpg_app_client(app_name) end def parse_schedule_time(time_str) - hour, tz = time_str.match(/([0-2][0-9]):00 (.*)/) && [ $1, $2 ] + hour, tz = time_str.match(/([0-2][0-9]):00 ?(.*)/) && [ $1, $2 ] if hour.nil? || tz.nil? abort("Invalid schedule format: expected ':00 '") end diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 8a6b5f980..ce955e438 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -163,6 +163,63 @@ module Heroku::Command end end + describe "heroku pg:backups schedule" do + before do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments).and_return(example_attachments) + end + + it "schedules the requested database at the specified time" do + stub_pg.schedule({ hour: '07', timezone: 'UTC', + schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) + stderr, stdout = execute("pg:backups schedule RED --at 07:00UTC --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + + it "finds the right database when there are similarly-named databases" do + additional_attachment = Heroku::Helpers::HerokuPostgresql::Attachment + .new({ + 'app' => {'name' => 'example'}, + 'name' => 'ALSO_HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'ALSO_HEROKU_POSTGRESQL_IVORY_URL', + 'resource' => {'name' => 'loudly-yelling-1239', + 'value' => 'postgres:///not-actually-ivory', + 'type' => 'heroku-postgresql:standard-0' }}) + example_attachments << additional_attachment + stub_pg.schedule({ hour: '07', timezone: 'UTC', + schedule_name: 'HEROKU_POSTGRESQL_IVORY_URL' }) + stderr, stdout = execute("pg:backups schedule HEROKU_POSTGRESQL_IVORY_URL --at 07:00UTC --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + + context "demonstrating cultural imperialism" do + { + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + 'MST' => 'America/Boise', + 'MDT' => 'America/Boise', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York', + 'Z' => 'UTC', + 'GMT' => 'Europe/London', + 'BST' => 'Europe/London', + }.each do |common_but_ambiguous_abbreviation, official_tz_db_name| + it "translates #{common_but_ambiguous_abbreviation} to #{official_tz_db_name}" do + stub_pg.schedule({ hour: '07', timezone: official_tz_db_name, + schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) + specified_time = "07:00#{common_but_ambiguous_abbreviation}" + stderr, stdout = execute("pg:backups schedule RED --at #{specified_time} --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + end + end + end + describe "heroku pg:backups unschedule" do let(:schedules) do [ { name: 'HEROKU_POSTGRESQL_GREEN_URL', From f6156f9ef4c3c3c5c89f4ca74c07d2e8de3532d9 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 21 Jul 2015 15:21:32 -0700 Subject: [PATCH 636/952] Use shellwords; avoid mangling CLI commands --- spec/heroku/command/pg_backups_spec.rb | 8 ++++---- spec/spec_helper.rb | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index ce955e438..a00126e9f 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -172,7 +172,7 @@ module Heroku::Command it "schedules the requested database at the specified time" do stub_pg.schedule({ hour: '07', timezone: 'UTC', schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) - stderr, stdout = execute("pg:backups schedule RED --at 07:00UTC --app example") + stderr, stdout = execute("pg:backups schedule RED --at '07:00 UTC' --app example") expect(stderr).to be_empty expect(stdout).to match(/Scheduled automatic daily backups/) end @@ -189,7 +189,7 @@ module Heroku::Command example_attachments << additional_attachment stub_pg.schedule({ hour: '07', timezone: 'UTC', schedule_name: 'HEROKU_POSTGRESQL_IVORY_URL' }) - stderr, stdout = execute("pg:backups schedule HEROKU_POSTGRESQL_IVORY_URL --at 07:00UTC --app example") + stderr, stdout = execute("pg:backups schedule HEROKU_POSTGRESQL_IVORY_URL --at '07:00 UTC' --app example") expect(stderr).to be_empty expect(stdout).to match(/Scheduled automatic daily backups/) end @@ -211,8 +211,8 @@ module Heroku::Command it "translates #{common_but_ambiguous_abbreviation} to #{official_tz_db_name}" do stub_pg.schedule({ hour: '07', timezone: official_tz_db_name, schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) - specified_time = "07:00#{common_but_ambiguous_abbreviation}" - stderr, stdout = execute("pg:backups schedule RED --at #{specified_time} --app example") + specified_time = "07:00 #{common_but_ambiguous_abbreviation}" + stderr, stdout = execute("pg:backups schedule RED --at '#{specified_time}' --app example") expect(stderr).to be_empty expect(stdout).to match(/Scheduled automatic daily backups/) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e92d25ae6..b94aa5d5e 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require "fakefs/safe" require 'tmpdir' require "webmock/rspec" +require "shellwords" include WebMock::API @@ -46,7 +47,7 @@ def prepare_command(klass) def execute(command_line, opts={}) extend RR::Adapters::RRMethods - args = command_line.split(" ") + args = command_line.shellsplit command = args.shift Heroku::Command.load From 7b965928e5359ee02ec22d41b43068c7362cac36 Mon Sep 17 00:00:00 2001 From: geemus Date: Wed, 22 Jul 2015 09:53:32 -0500 Subject: [PATCH 637/952] v3.40.5 --- CHANGELOG | 9 +++++++++ lib/heroku/version.rb | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index fb52161f7..ded8de2fa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +3.40.5 2015-07-22 +================= +Add space option to apps, apps:info, and apps:create +Change addons:add to addons:create +Clarify release guide +Display provider messages for addons upgrade/downgrade +block specifying both space and org to app list + + 3.39.5 2015-07-10 ================= Group attachments by resource in pg:info diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0700161c3..9360fbadf 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.39.5" + VERSION = "3.40.5" end From 98b0217125f2741ea1befe574a481075cb0b06a2 Mon Sep 17 00:00:00 2001 From: geemus Date: Wed, 22 Jul 2015 09:58:55 -0500 Subject: [PATCH 638/952] update Gemfile.lock --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 882f4dc15..3a3b0d83c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.39.5) + heroku (3.40.5) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) @@ -28,7 +28,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.45.2) + excon (0.45.4) fakefs (0.6.7) heroku-api (0.3.23) excon (~> 0.44) From f1a81cd6cb5d56f4b3e9c707f9861c2d911ec8c5 Mon Sep 17 00:00:00 2001 From: tef Date: Wed, 22 Jul 2015 11:06:08 -0700 Subject: [PATCH 639/952] Remove pgbackups --- lib/heroku/client/pgbackups.rb | 112 -------- lib/heroku/command/pgbackups.rb | 355 ++------------------------ spec/heroku/client/pgbackups_spec.rb | 43 ---- spec/heroku/command/pgbackups_spec.rb | 310 +--------------------- 4 files changed, 26 insertions(+), 794 deletions(-) delete mode 100644 lib/heroku/client/pgbackups.rb delete mode 100644 spec/heroku/client/pgbackups_spec.rb diff --git a/lib/heroku/client/pgbackups.rb b/lib/heroku/client/pgbackups.rb deleted file mode 100644 index a031daa9d..000000000 --- a/lib/heroku/client/pgbackups.rb +++ /dev/null @@ -1,112 +0,0 @@ -require "heroku/client" - -class Heroku::Client::Pgbackups - - include Heroku::Helpers - - def initialize(uri) - @uri = URI.parse(uri) - end - - def authenticated_resource(path) - host = "#{@uri.scheme}://#{@uri.host}" - host += ":#{@uri.port}" if @uri.port - RestClient::Resource.new("#{host}#{path}", - :user => @uri.user, - :password => @uri.password, - :headers => {:x_heroku_gem_version => Heroku::Client.version} - ) - end - - def create_transfer(from_url, from_name, to_url, to_name, opts={}) - # opts[:expire] => true will delete the oldest backup if at the plan limit - resource = authenticated_resource("/client/transfers") - params = {:from_url => from_url, :from_name => from_name, :to_url => to_url, :to_name => to_name}.merge opts - json_decode post(resource, params).body - end - - def get_transfers - resource = authenticated_resource("/client/transfers") - json_decode get(resource).body - end - - def get_transfer(id) - resource = authenticated_resource("/client/transfers/#{id}") - json_decode get(resource).body - end - - def get_backups(opts={}) - resource = authenticated_resource("/client/backups") - json_decode get(resource).body - end - - def get_backup(name, opts={}) - name = URI.escape(name) - resource = authenticated_resource("/client/backups/#{name}") - json_decode get(resource).body - end - - def get_latest_backup - resource = authenticated_resource("/client/latest_backup") - json_decode get(resource).body - end - - def delete_backup(name) - name = URI.escape(name) - begin - resource = authenticated_resource("/client/backups/#{name}") - delete(resource).body - true - rescue RestClient::ResourceNotFound => e - false - end - end - - private - - def get(resource) - check_errors do - response = resource.get - display_heroku_warning response - response - end - end - - def post(resource, params) - check_errors do - response = resource.post(params) - display_heroku_warning response - response - end - end - - def delete(resource) - check_errors do - response = resource.delete - display_heroku_warning response - response - end - end - - def check_errors - yield - rescue RestClient::Unauthorized - error "Invalid PGBACKUPS_URL" - end - - def display_heroku_warning(response) - warning = response.headers[:x_heroku_warning] - display warning if warning - response - end - -end - -module Pgbackups - class Client < Heroku::Client::Pgbackups - def initialize(*args) - Heroku::Helpers.deprecate "Pgbackups::Client has been deprecated. Please use Heroku::Client::Pgbackups instead." - super - end - end -end diff --git a/lib/heroku/command/pgbackups.rb b/lib/heroku/command/pgbackups.rb index 2673b0b6d..5ae3888e2 100644 --- a/lib/heroku/command/pgbackups.rb +++ b/lib/heroku/command/pgbackups.rb @@ -1,42 +1,19 @@ -require "heroku/client/pgbackups" require "heroku/command/base" -require "heroku/helpers/heroku_postgresql" module Heroku::Command # manage backups of heroku postgresql databases - class Pgbackups < Base - - include Heroku::Helpers::HerokuPostgresql + # removed: see heroku pg:backups + class Pgbackups < Base # pgbackups # # list captured backups # def index - validate_arguments! - - backups = [] - pgbackup_client.get_transfers.each { |t| - next unless backup_types.member?(t['to_name']) && !t['error_at'] && !t['destroyed_at'] - backups << { - 'id' => backup_name(t['to_url']), - 'started_at' => t['started_at'], - 'status' => transfer_status(t), - 'size' => t['size'], - 'database' => t['from_name'] - } - } - - if backups.empty? - no_backups_error! - else - display_table( - backups, - %w{ id started_at status size database }, - ["ID", "Backup Time", "Status", "Size", "Database"] - ) - end + error("'heroku pgbackups' has been removed. +Please see 'heroku pg:backups' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:url [BACKUP_ID] @@ -44,22 +21,9 @@ def index # get a temporary URL for a backup # def url - name = shift_argument - validate_arguments! - - if name - b = pgbackup_client.get_backup(name) - else - b = pgbackup_client.get_latest_backup - end - unless b['public_url'] - error("No backup found.") - end - if $stdout.isatty - display '"'+b['public_url']+'"' - else - display b['public_url'] - end + error("'heroku pgbackups url' has been removed. +Please see 'heroku pg:backups public-url' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:capture [DATABASE] @@ -71,31 +35,9 @@ def url # -e, --expire # if no slots are available, destroy the oldest manual backup to make room # def capture - attachment = resolve(shift_argument, "DATABASE_URL") - validate_arguments! - - from_name = attachment.display_name - from_url = attachment.url - to_url = nil # server will assign - to_name = "BACKUP" - - opts = {:expire => options[:expire]} - - backup = transfer!(from_url, from_name, to_url, to_name, opts) - - to_uri = URI.parse backup["to_url"] - backup_id = to_uri.path.empty? ? "error" : File.basename(to_uri.path, '.*') - display "\n#{from_name} ----backup---> #{backup_id}" - - backup = poll_transfer!(backup) - - if backup["error_at"] - message = "An error occurred and your backup did not finish." - message += "\nPlease run `heroku logs --ps pgbackups` for details." - message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/ - message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/ - error(message) - end + error("'heroku pgbackups capture' has been removed. +Please see 'heroku pg:backups capture' instead. The '-e/--expire' flag is no longer supported. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:restore [ [BACKUP_ID|BACKUP_URL]] @@ -106,67 +48,9 @@ def capture # if DATABASE is specified, but no BACKUP_ID, defaults to latest backup # def restore - if 0 == args.size - attachment = resolve(nil, "DATABASE_URL") - to_name = attachment.display_name - to_url = attachment.url - backup_id = :latest - elsif 1 == args.size - attachment = resolve(shift_argument) - to_name = attachment.display_name - to_url = attachment.url - backup_id = :latest - else - attachment = resolve(shift_argument) - to_name = attachment.display_name - to_url = attachment.url - backup_id = shift_argument - end - - if :latest == backup_id - backup = pgbackup_client.get_latest_backup - no_backups_error! if {} == backup - to_uri = URI.parse backup["to_url"] - backup_id = File.basename(to_uri.path, '.*') - backup_id = "#{backup_id} (most recent)" - from_url = backup["to_url"] - from_name = "BACKUP" - elsif backup_id =~ /^http(s?):\/\// - from_url = backup_id - from_name = "EXTERNAL_BACKUP" - from_uri = URI.parse backup_id - backup_id = from_uri.path.empty? ? from_uri : File.basename(from_uri.path) - else - backup = pgbackup_client.get_backup(backup_id) - abort("Backup #{backup_id} already destroyed.") if backup["destroyed_at"] - - from_url = backup["to_url"] - from_name = "BACKUP" - end - - message = "#{to_name} <---restore--- " - padding = " " * message.length - display "\n#{message}#{backup_id}" - if backup - display padding + "#{backup['from_name']}" - display padding + "#{backup['created_at']}" - display padding + "#{backup['size']}" - end - - if confirm_command - restore = transfer!(from_url, from_name, to_url, to_name) - restore = poll_transfer!(restore) - - if restore["error_at"] - message = "An error occurred and your restore did not finish." - if restore['log'] =~ /Invalid dump format: .*: XML document text/ - message += "\nThe backup url is invalid. Use `pgbackups:url` to generate a new temporary URL." - else - message += "\nPlease run `heroku logs --ps pgbackups` for details." - end - error(message) - end - end + error("'heroku pgbackups restore' has been removed. +Please see 'heroku pg:backups restore' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:destroy BACKUP_ID @@ -174,17 +58,9 @@ def restore # destroys a backup # def destroy - unless name = shift_argument - error("Usage: heroku pgbackups:destroy BACKUP_ID\nMust specify BACKUP_ID to destroy.") - end - backup = pgbackup_client.get_backup(name) - if backup["destroyed_at"] - error("Backup #{name} already destroyed.") - end - - action("Destroying #{name}") do - pgbackup_client.delete_backup(name) - end + error("'heroku pgbackups destroy' has been removed. +Please see 'heroku pg:backups delete' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end # pgbackups:transfer [SOURCE DATABASE] DESTINATION DATABASE @@ -202,200 +78,9 @@ def destroy #$ heroku pgbackups:transfer DATABASE postgres://user:password@host/dbname --app example # def transfer - db1 = shift_argument - db2 = shift_argument - - if db1.nil? - error("pgbackups:transfer requires at least one argument") - end - - if db2.nil? - db2 = db1 - db1 = "DATABASE_URL" - end - - from = resolve_transfer(db1) - to = resolve_transfer(db2) - - validate_arguments! - - if from.url == to.url - error("source and target database are the same") - end - - opts = {} - verify_app = to.app || app - if confirm_command(verify_app, "WARNING: Destructive Action\nTransferring data from #{from.name} to #{to.name}") - backup = transfer!(from.url, from.name, to.url, to.name, opts) - backup = poll_transfer!(backup) - - if backup["error_at"] - message = "An error occurred and your backup did not finish." - message += "\nThe database is not yet online. Please try again." if backup['log'] =~ /Name or service not known/ - message += "\nThe database credentials are incorrect." if backup['log'] =~ /psql: FATAL:/ - error(message) - end - end - end - protected - - def transfer_status(t) - if t['finished_at'] - "Finished @ #{t["finished_at"]}" - elsif t['started_at'] - step = t['progress'] && t['progress'].split[0] - step.nil? ? 'Unknown' : step_map[step] - else - "Unknown" - end - end - - def config_vars - @config_vars ||= api.get_config_vars(app).body - end - - def pgbackup_client - pgbackups_url = config_vars["PGBACKUPS_URL"] - error("Please add the pgbackups addon first via:\nheroku addons:create pgbackups") unless pgbackups_url - @pgbackup_client ||= Heroku::Client::Pgbackups.new(pgbackups_url) - end - - def backup_name(to_url) - # translate s3://bucket/email/foo/bar.dump => foo/bar - parts = to_url.split('/') - parts.slice(4..-1).join('/').gsub(/\.dump$/, '') - end - - def transfer!(from_url, from_name, to_url, to_name, opts={}) - pgbackup_client.create_transfer(from_url, from_name, to_url, to_name, opts) - end - - def poll_error(app) - error <<-EOM -Failed to query the PGBackups status API. Your backup may still be running. -Verify the status of your backup with `heroku pgbackups -a #{app}` -You can also watch progress with `heroku logs --tail --ps pgbackups -a #{app}` - EOM - end - - def poll_transfer!(transfer) - display "\n" - - if transfer["errors"] - transfer["errors"].values.flatten.each { |e| - output_with_bang "#{e}" - } - abort - end - - transfer_id = transfer["id"] - - while true - unless transfer.nil? - update_display(transfer) - break if transfer["finished_at"] - end - - sleep_time = 1 - begin - sleep(sleep_time) - transfer = pgbackup_client.get_transfer(transfer_id) - rescue - if sleep_time > 300 - poll_error(app) - else - sleep_time *= 2 - retry - end - end - end - - display "\n" - - return transfer - end - - def step_map - @step_map ||= { - "dump" => "Capturing", - "upload" => "Storing", - "download" => "Retrieving", - "restore" => "Restoring", - "gunzip" => "Uncompressing", - "load" => "Restoring", - } - end - - def update_display(transfer) - @ticks ||= 0 - @last_updated_at ||= 0 - @last_logs ||= [] - @last_progress ||= ["", 0] - - @ticks += 1 - - if !transfer["log"] - @last_progress = ['pending', nil] - redisplay "Pending... #{spinner(@ticks)}" - else - logs = transfer["log"].split("\n") - new_logs = logs - @last_logs - @last_logs = logs - - new_logs.each do |line| - matches = line.scan /^([a-z_]+)_progress:\s+([^ ]+)/ - next if matches.empty? - - step, amount = matches[0] - - if ['done', 'error'].include? amount - # step is done, explicitly print result and newline - redisplay "#{@last_progress[0].capitalize}... #{amount}\n" - end - - # store progress, last one in the logs will get displayed - step = step_map[step] || step - @last_progress = [step, amount] - end - - step, amount = @last_progress - unless ['done', 'error'].include? amount - redisplay "#{step.capitalize}... #{amount} #{spinner(@ticks)}" - end - end - end - - private - - TransferEndpoint = Struct.new(:url, :name, :app) - - # - # resolve the given database identifier - def resolve_transfer(db) - if /^postgres:/ =~ db - uri = URI.parse(db) - TransferEndpoint.new(uri, "Database on #{uri.host}:#{uri.port || 5432}#{uri.path}") - else - attachment = resolve(db) - TransferEndpoint.new(attachment.url, db.upcase, attachment.app) - end - end - - def resolve(identifer, default=nil) - Resolver.new(app, api).resolve(identifer, default) - end - - def no_backups_error! - error("No backups. Capture one with `heroku pgbackups:capture`.") - end - - # lists all types of backups ('to_name' attribute) - # - # Useful when one doesn't care if a backup is of a particular - # kind, but wants to know what backups of any kind exist. - # - def backup_types - %w[BACKUP DAILY_SCHEDULED_BACKUP HOURLY_SCHEDULED_BACKUP AUTO_SCHEDULED_BACKUP] + error("'heroku pgbackups:transfer' has been removed. +Please see 'heroku pg:copy' instead. +More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups") end end end diff --git a/spec/heroku/client/pgbackups_spec.rb b/spec/heroku/client/pgbackups_spec.rb deleted file mode 100644 index e4fa794cf..000000000 --- a/spec/heroku/client/pgbackups_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require "spec_helper" -require "heroku/client/pgbackups" -require "heroku/helpers" - -describe Heroku::Client::Pgbackups do - - include Heroku::Helpers - - let(:path) { "http://id:password@pgbackups.heroku.com" } - let(:client) { Heroku::Client::Pgbackups.new path+'/api' } - let(:transfer_path) { path + '/client/transfers' } - - describe "api" do - let(:version) { Heroku::Client.version } - - it 'still has a heroku gem version' do - expect(version).to be - expect(version.split(/\./).first.to_i).to be >= 2 - end - - it 'includes the heroku gem version' do - stub_request(:get, transfer_path) - client.get_transfers - expect(a_request(:get, transfer_path).with( - :headers => {'X-Heroku-Gem-Version' => version} - )).to have_been_made.once - end - end - - describe "create transfers" do - it "sends a request to the client" do - stub_request(:post, transfer_path).to_return( - :body => json_encode({"message" => "success"}), - :status => 200 - ) - - client.create_transfer("postgres://from", "postgres://to", "FROMNAME", "TO_NAME") - - expect(a_request(:post, transfer_path)).to have_been_made.once - end - end - -end diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index 5db07aa4d..d3e0c0c28 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -2,313 +2,15 @@ require "heroku/command/pgbackups" module Heroku::Command - describe Pgbackups, 'with no databases' do - it "aborts if no database addon is present" do - api.post_app("name" => "example") - stub_core - stderr, stdout = execute("pgbackups:capture") + describe Pgbackups, 'is removed' do + it "does not list" do + stderr, stdout = execute("pgbackups") expect(stderr).to eq <<-STDERR - ! Your app has no databases. + ! 'heroku pgbackups' has been removed. + ! Please see 'heroku pg:backups' instead. + ! More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups STDERR expect(stdout).to eq("") - api.delete_app("example") - end - end - - describe Pgbackups do - before do - @pgbackups = prepare_command(Pgbackups) - allow(@pgbackups.heroku).to receive(:info).and_return({}) - - api.post_app("name" => "example") - api.put_config_vars( - "example", - { - "DATABASE_URL" => "postgres://database", - "HEROKU_POSTGRESQL_IVORY" => "postgres://database", - "PGBACKUPS_URL" => "https://ip:password@pgbackups.heroku.com/client" - } - ) - any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) do |pg| - stub(pg).app_attachments.returns(mock_attachments) - stub(pg).api.returns(api) - end - end - - let(:mock_attachments) { - [ - Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, - 'name' => 'HEROKU_POSTGRESQL_IVORY', - 'config_var' => 'HEROKU_POSTGRESQL_IVORY', - 'resource' => {'name' => 'softly-mocking-123', - 'value' => 'postgres://database', - 'type' => 'heroku-postgresql:baku' }}) - ] - } - - after do - api.delete_app("example") - end - - it "requests a pgbackups transfer list for the index command" do - stub_core - stub_pgbackups.get_transfers.returns([{ - "created_at" => "2012-01-01 12:00:00 +0000", - "started_at" => "2012-01-01 12:00:01 +0000", - "from_name" => "DATABASE", - "size" => "1024", - "progress" => "dump 2048", - "to_name" => "BACKUP", - "to_url" => "s3://bucket/userid/b001.dump" - }]) - - stderr, stdout = execute("pgbackups") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -ID Backup Time Status Size Database ----- ------------------------- --------- ---- -------- -b001 2012-01-01 12:00:01 +0000 Capturing 1024 DATABASE -STDOUT - end - - describe "single backup" do - let(:from_name) { "FROM_NAME" } - let(:from_url) { "postgres://from/bar" } - let(:attachment) { double('attachment', :display_name => from_name, :url => from_url ) } - before do - allow(@pgbackups).to receive(:resolve).and_return(attachment) - end - - it "gets the url for the latest backup if nothing is specified" do - stub_core - stub_pgbackups.get_latest_backup.returns({"public_url" => "http://latest/backup.dump"}) - - old_stdout_isatty = STDOUT.isatty - allow($stdout).to receive(:isatty).and_return(true) - stderr, stdout = execute("pgbackups:url") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -http://latest/backup.dump -STDOUT - allow($stdout).to receive(:isatty).and_return(old_stdout_isatty) - end - - it "gets the url for the named backup if a name is specified" do - stub_pgbackups.get_backup.with("b001").returns({"public_url" => "http://latest/backup.dump" }) - - old_stdout_isatty = STDOUT.isatty - allow($stdout).to receive(:isatty).and_return(true) - stderr, stdout = execute("pgbackups:url b001") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -http://latest/backup.dump -STDOUT - allow($stdout).to receive(:isatty).and_return(old_stdout_isatty) - end - - it "should capture a backup when requested" do - backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - - allow(@pgbackups).to receive(:args).and_return([]) - allow(@pgbackups).to receive(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => nil}).and_return(backup_obj) - allow(@pgbackups).to receive(:poll_transfer!).with(backup_obj).and_return(backup_obj) - - @pgbackups.capture - end - - it "should send expiration flag to client if specified on args" do - backup_obj = {'to_url' => "s3://bucket/userid/b001.dump"} - - allow(@pgbackups).to receive(:options).and_return({:expire => true}) - allow(@pgbackups).to receive(:transfer!).with(from_url, from_name, nil, "BACKUP", {:expire => true}).and_return(backup_obj) - allow(@pgbackups).to receive(:poll_transfer!).with(backup_obj).and_return(backup_obj) - - @pgbackups.capture - end - - it "destroys no backup without a name" do - stub_core - stderr, stdout = execute("pgbackups:destroy") - expect(stderr).to eq <<-STDERR - ! Usage: heroku pgbackups:destroy BACKUP_ID - ! Must specify BACKUP_ID to destroy. -STDERR - expect(stdout).to eq("") - end - - it "destroys a backup" do - stub_core - stub_pgbackups.get_backup("b001").returns({}) - stub_pgbackups.delete_backup("b001").returns({}) - - stderr, stdout = execute("pgbackups:destroy b001") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Destroying b001... done -STDOUT - end - - - context "on errors" do - def stub_failed_capture(log) - @backup_obj = { - "error_at" => Time.now.to_s, - "finished_at" => Time.now.to_s, - "log" => log, - 'to_url' => 'postgres://from/bar' - } - stub_core - stub_pgbackups.create_transfer.returns(@backup_obj) - stub_pgbackups.get_transfer.returns(@backup_obj) - - any_instance_of(Heroku::Command::Pgbackups) do |pgbackups| - stub(pgbackups).app_attachments.returns( - mock_attachments - ) - end - end - - it 'aborts on a generic error' do - stub_failed_capture "something generic" - stderr, stdout = execute("pgbackups:capture") - expect(stderr).to eq <<-STDERR - ! An error occurred and your backup did not finish. - ! Please run `heroku logs --ps pgbackups` for details. -STDERR - expect(stdout).to eq <<-STDOUT - -HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar - -\r\e[0K... 0 - -STDOUT - end - - it 'aborts and informs when the database isnt up yet' do - stub_failed_capture 'could not translate host name "ec2-42-42-42-42.compute-1.amazonaws.com" to address: Name or service not known' - stderr, stdout = execute("pgbackups:capture") - expect(stderr).to eq <<-STDERR - ! An error occurred and your backup did not finish. - ! Please run `heroku logs --ps pgbackups` for details. - ! The database is not yet online. Please try again. -STDERR - expect(stdout).to eq <<-STDOUT - -HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar - -\r\e[0K... 0 - -STDOUT - end - - it 'aborts and informs when the credentials are incorrect' do - stub_failed_capture 'psql: FATAL: database "randomname" does not exist' - stderr, stdout = execute("pgbackups:capture") - expect(stderr).to eq <<-STDERR - ! An error occurred and your backup did not finish. - ! Please run `heroku logs --ps pgbackups` for details. - ! The database credentials are incorrect. -STDERR - expect(stdout).to eq <<-STDOUT - -HEROKU_POSTGRESQL_IVORY (DATABASE_URL) ----backup---> bar - -\r\e[0K... 0 - -STDOUT - end - end - end - - context "restore" do - let(:attachment) { double('attachment', :display_name => 'someconfigvar', :url => 'postgres://fromhost/database') } - before do - @pgbackups_client = double("pgbackups_client") - allow(@pgbackups).to receive(:pgbackup_client).and_return(@pgbackups_client) - end - - it "should receive a confirm_command on restore" do - allow(@pgbackups_client).to receive(:get_latest_backup) { {"to_url" => "s3://bucket/user/bXXX.dump"} } - - expect(@pgbackups).to receive(:confirm_command).and_return(false) - expect(@pgbackups_client).not_to receive(:transfer!) - - @pgbackups.restore - end - - it "aborts if no database addon is present" do - expect(@pgbackups).to receive(:resolve).and_raise(SystemExit) - expect { @pgbackups.restore }.to raise_error(SystemExit) - end - - context "for commands which perform restores" do - before do - @backup_obj = { - "to_name" => "TO_NAME", - "to_url" => "s3://bucket/userid/bXXX.dump", - "from_url" => "FROM_NAME", - "from_name" => "postgres://databasehost/dbname" - } - - allow(@pgbackups).to receive(:confirm_command).and_return(true) - expect(@pgbackups_client).to receive(:create_transfer).and_return(@backup_obj) - allow(@pgbackups).to receive(:poll_transfer!).and_return(@backup_obj) - end - - it "should default to the latest backup" do - allow(@pgbackups).to receive(:args).and_return([]) - mock(@pgbackups_client).get_latest_backup.returns(@backup_obj) - @pgbackups.restore - end - - - it "should restore the named backup" do - name = "backupname" - args = ['DATABASE', name] - allow(@pgbackups).to receive(:args).and_return(args) - allow(@pgbackups).to receive(:shift_argument).and_return(*args) - allow(@pgbackups).to receive(:resolve).and_return(attachment) - mock(@pgbackups_client).get_backup.with(name).returns(@backup_obj) - @pgbackups.restore - end - - it "should handle external restores" do - args = ['db_name_gets_shifted_out_in_resolve_db', 'http://external/file.dump'] - allow(@pgbackups).to receive(:args).and_return(args) - allow(@pgbackups).to receive(:shift_argument).and_return(*args) - allow(@pgbackups).to receive(:resolve).and_return(attachment) - expect(@pgbackups_client).not_to receive(:get_backup) - expect(@pgbackups_client).not_to receive(:get_latest_backup) - @pgbackups.restore - end - end - - context "on errors" do - before(:each) do - allow(@pgbackups_client).to receive(:get_latest_backup).and_return("to_url" => "s3://bucket/user/bXXX.dump") - allow(@pgbackups).to receive(:confirm_command).and_return(true) - end - - def stub_error_backup_with_log(log) - @backup_obj = { - "error_at" => Time.now.to_s, - "log" => log - } - - expect(@pgbackups_client).to receive(:create_transfer) { @backup_obj } - allow(@pgbackups).to receive(:poll_transfer!) { @backup_obj } - end - - it 'aborts for a generic error' do - stub_error_backup_with_log 'something generic' - expect(@pgbackups).to receive(:error).with("An error occurred and your restore did not finish.\nPlease run `heroku logs --ps pgbackups` for details.") - @pgbackups.restore - end - - it 'aborts and informs for expired s3 urls' do - stub_error_backup_with_log 'Invalid dump format: /tmp/aDMyoXPrAX/b031.dump: XML document text' - expect(@pgbackups).to receive(:error).with(/backup url is invalid/) - @pgbackups.restore - end - end end end end From 6c643cd39b99946d89264f12a54ee4a0df8c2436 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 22 Jul 2015 12:11:24 -0700 Subject: [PATCH 640/952] Fix app space command to work with default HEROKU_ORGANIZATION set in ENV --- lib/heroku/command/apps.rb | 39 ++++--- lib/heroku/helpers.rb | 1 + spec/heroku/command/apps_spec.rb | 168 +++++++++++++++++++------------ 3 files changed, 130 insertions(+), 78 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index cc0c073ab..973e3e88c 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -27,12 +27,9 @@ class Heroku::Command::Apps < Heroku::Command::Base # def index validate_arguments! + validate_space_xor_org! options[:ignore_no_org] = true - if options[:space] && org - error "Specify option for space or org, but not both." - end - apps = if options[:space] api.get_apps.body.select do |app| app["space"] && [app["space"]["name"], app["space"]["id"]].include?(options[:space]) @@ -44,7 +41,10 @@ def index end unless apps.empty? - if org + if options[:space] + styled_header("Apps in space #{options[:space]}") + styled_array(apps.map { |app| regionized_app_name(app) }) + elsif org joined, unjoined = apps.partition { |app| app['joined'] == true } styled_header("Apps joined in organization #{org}") @@ -65,9 +65,6 @@ def index display end end - elsif options[:space] - styled_header("Apps in space #{options[:space]}") - styled_array(apps.map { |app| regionized_app_name(app) }) else my_apps, collaborated_apps = apps.partition { |app| app["owner_email"] == Heroku::Auth.user } @@ -82,10 +79,10 @@ def index end end else - if org then - display("There are no apps in organization #{org}.") - elsif options[:space] + if options[:space] display("There are no apps available in space #{options[:space]}.") + elsif org + display("There are no apps in organization #{org}.") else display("You have no apps.") end @@ -248,6 +245,7 @@ def info def create name = shift_argument || options[:app] || ENV['HEROKU_APP'] validate_arguments! + validate_space_xor_org! options[:ignore_no_org] = true params = { @@ -258,17 +256,21 @@ def create "locked" => options[:locked] } - info = if org - org_api.post_app(params, org).body - elsif options[:space] + info = if options[:space] api.post_organizations_app_v3_dogwood(params).body + elsif org + org_api.post_app(params, org).body else api.post_app(params).body end begin - space_action = info['space'] ? " in space #{info['space']['name']}" : '' - action("Creating #{info['name']}#{space_action}", :org => !!org) do + display_org = !!org + if info['space'] + space_name = info['space']['name'] + display_org = false + end + action("Creating #{info['name']}", :space => space_name, :org => display_org) do if info['create_status'] == 'creating' Timeout::timeout(options[:timeout].to_i) do loop do @@ -526,4 +528,9 @@ def region_from_app app region = app["region"].is_a?(Hash) ? app["region"]["name"] : app["region"] end + def validate_space_xor_org! + if options[:space] && options[:org] + error "Specify option for space or org, but not both." + end + end end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 216e057da..d9fbc0a35 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -253,6 +253,7 @@ def fail(message) ## DISPLAY HELPERS def action(message, options={}) + message = "#{message} in space #{options[:space]}" if options[:space] message = "#{message} in organization #{org}" if options[:org] display("#{message}... ", false) Heroku::Helpers.error_with_failure = true diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 034572afb..3ba442d7f 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -7,6 +7,7 @@ module Heroku::Command before(:each) do stub_core stub_organizations + ENV.delete('HEROKU_ORGANIZATION') end context("info") do @@ -163,35 +164,54 @@ module Heroku::Command api.delete_app("alternate-remote") end - it "with a space" do - Excon.stub( - :headers => { 'Accept' => 'application/vnd.heroku+json; version=3.dogwood'}, - :method => :post, - :path => '/organizations/apps') do - { - :status => 201, - :body => { - :name => 'spaceapp', - :space => { - :name => 'example-space' - }, - :stack => 'cedar-14', - :web_url => 'http://spaceapp.herokuapp.com/' - }.to_json, - } - end + context "with a space" do + shared_examples "create in a space" do + Excon.stub( + :headers => { 'Accept' => 'application/vnd.heroku+json; version=3.dogwood'}, + :method => :post, + :path => '/organizations/apps') do + { + :status => 201, + :body => { + :name => 'spaceapp', + :space => { + :name => 'example-space' + }, + :stack => 'cedar-14', + :web_url => 'http://spaceapp.herokuapp.com/' + }.to_json, + } + end - with_blank_git_repository do - stderr, stdout = execute("apps:create spaceapp --space example-space") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "creates app in space" do + with_blank_git_repository do + stderr, stdout = execute("apps:create spaceapp --space example-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT Creating spaceapp in space example-space... done, stack is cedar-14 http://spaceapp.herokuapp.com/ | https://git.heroku.com/spaceapp.git Git remote heroku added - STDOUT + STDOUT + end + end end - end + context "with default org" do + before(:each) do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + end + + it_behaves_like "create in a space" + end + + context "without default org" do + before(:each) do + ENV.delete('HEROKU_ORGANIZATION') + end + + it_behaves_like "create in a space" + end + end end context("index") do @@ -269,65 +289,83 @@ module Heroku::Command end context("index with space") do - context("and the space has no apps") do - before(:each) do - @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do - { - :body => MultiJson.dump([]), - :status => 200 - } + shared_examples "index with space" do + context("and the space has no apps") do + before(:each) do + @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do + { + :body => MultiJson.dump([]), + :status => 200 + } + end end - end - after(:each) do - Excon.stubs.delete(@space_apps_stub) - end + after(:each) do + Excon.stubs.delete(@space_apps_stub) + end - it "displays a message when the space has no apps" do - stderr, stdout = execute("apps --space test-space") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "displays a message when the space has no apps" do + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT There are no apps available in space test-space. STDOUT + end end - end - context("and the space has apps") do - before(:each) do - @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do - { - :body => MultiJson.dump([ - {"name" => "space-app-1", "space" => {"id" => "test-space-id", "name" => "test-space"}}, - {"name" => "non-space-app-2", "space" => nil} - ]), - :status => 200 - } + context("and the space has apps") do + before(:each) do + @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do + { + :body => MultiJson.dump([ + {"name" => "space-app-1", "space" => {"id" => "test-space-id", "name" => "test-space"}}, + {"name" => "non-space-app-2", "space" => nil} + ]), + :status => 200 + } + end end - end - after(:each) do - Excon.stubs.delete(@space_apps_stub) - end + after(:each) do + Excon.stubs.delete(@space_apps_stub) + end - it "lists only apps in spaces by name" do - stderr, stdout = execute("apps --space test-space") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "lists only apps in spaces by name" do + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Apps in space test-space space-app-1 STDOUT - end + end - it "lists only apps in spaces by id" do - stderr, stdout = execute("apps --space test-space-id") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT + it "lists only apps in spaces by id" do + stderr, stdout = execute("apps --space test-space-id") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT === Apps in space test-space-id space-app-1 STDOUT + end + end + end + + context "with default org" do + before(:each) do + ENV['HEROKU_ORGANIZATION'] = 'test-org' end + + it_behaves_like "index with space" + end + + context "without default org" do + before(:each) do + ENV.delete('HEROKU_ORGANIZATION') + end + + it_behaves_like "index with space" end end @@ -339,6 +377,12 @@ module Heroku::Command ! Specify option for space or org, but not both. STDERR end + + it "does not display error if org specified via env" do + ENV['HEROKU_ORGANIZATION'] = 'test-org' + stderr, stdout = execute("apps --space test-space") + expect(stderr).to eq("") + end end context("rename") do From e2c0106c5a588ef79267439656bc6fe23aeb2371 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Wed, 22 Jul 2015 12:13:50 -0700 Subject: [PATCH 641/952] validate_space_xor_org! after ignoring no org --- lib/heroku/command/apps.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 973e3e88c..b7461ce9b 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -27,8 +27,8 @@ class Heroku::Command::Apps < Heroku::Command::Base # def index validate_arguments! - validate_space_xor_org! options[:ignore_no_org] = true + validate_space_xor_org! apps = if options[:space] api.get_apps.body.select do |app| @@ -245,8 +245,8 @@ def info def create name = shift_argument || options[:app] || ENV['HEROKU_APP'] validate_arguments! - validate_space_xor_org! options[:ignore_no_org] = true + validate_space_xor_org! params = { "name" => name, From 6720b82e35f89235451b4120a59e1a1ee935fc38 Mon Sep 17 00:00:00 2001 From: tef Date: Thu, 23 Jul 2015 10:51:03 -0700 Subject: [PATCH 642/952] Fix Spec --- spec/heroku/command/pgbackups_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/heroku/command/pgbackups_spec.rb b/spec/heroku/command/pgbackups_spec.rb index d3e0c0c28..bfbdc8f62 100644 --- a/spec/heroku/command/pgbackups_spec.rb +++ b/spec/heroku/command/pgbackups_spec.rb @@ -6,8 +6,8 @@ module Heroku::Command it "does not list" do stderr, stdout = execute("pgbackups") expect(stderr).to eq <<-STDERR - ! 'heroku pgbackups' has been removed. - ! Please see 'heroku pg:backups' instead. + ! 'heroku pgbackups' has been removed. + ! Please see 'heroku pg:backups' instead. ! More Information: https://devcenter.heroku.com/articles/heroku-postgres-backups STDERR expect(stdout).to eq("") From 87b438a8a8635d20aec4b3004ae731d1ce2fcc32 Mon Sep 17 00:00:00 2001 From: geemus Date: Thu, 23 Jul 2015 15:37:35 -0500 Subject: [PATCH 643/952] v3.40.6 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ded8de2fa..bcab14098 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.40.6 2015-07-23 +================= +Fix for space conflicts wrt HEROKU_ORGANIZATION + 3.40.5 2015-07-22 ================= Add space option to apps, apps:info, and apps:create diff --git a/Gemfile.lock b/Gemfile.lock index 3a3b0d83c..4e70129e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.5) + heroku (3.40.6) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9360fbadf..eaaca0580 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.5" + VERSION = "3.40.6" end From e441da696c6b3890c68d5e299593424e26ed4e22 Mon Sep 17 00:00:00 2001 From: tef Date: Wed, 22 Jul 2015 07:32:33 -0700 Subject: [PATCH 644/952] Completed with warnings --- lib/heroku/command/pg_backups.rb | 14 ++++++++++++-- spec/heroku/command/pg_backups_spec.rb | 16 +++++++++++++--- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 3bec2f16d..b3f797ad6 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -149,7 +149,12 @@ def transfer_num(transfer_name) def transfer_status(t) if t[:finished_at] && t[:succeeded] - "Finished #{t[:finished_at]}" + warnings = t[:warnings] + if warnings && warnings > 0 + "Completed with #{warnings} warnings" #{t[:finished_at]}" + else + "Finished #{t[:finished_at]}" + end elsif t[:finished_at] && !t[:succeeded] "Failed #{t[:finished_at]}" elsif t[:started_at] @@ -284,7 +289,12 @@ def backup_status client.transfers_get(backup_num, verbose) end status = if backup[:succeeded] - "Completed Successfully" + warnings = backup[:warnings] + if warnings && warnings > 0 + "Completed with #{warnings} warnings" + else + "Completed Successfully" + end elsif backup[:canceled_at] "Canceled" elsif backup[:finished_at] diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 8a6b5f980..55bec5c4c 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -240,14 +240,18 @@ module Heroku::Command :started_at => Time.now, :finished_at => Time.now, :from_name => "CRIMSON", :to_name => "CLOVER", :processed_bytes => 42, :succeeded => false }, - { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', + { :uuid => 'ffffffff-ffff-ffff-ffff-fffffffffffd', :from_name => from_name, :to_name => 'PGBACKUPS BACKUP', :num => 7, :logs => logs, :from_type => 'pg_dump', :to_type => 'gof3r', :started_at => started_at, :finished_at => finished_at, :processed_bytes => backup_size, :source_bytes => source_size, :options => { "pgbackups_name" => "b047" }, - :succeeded => true } + :succeeded => true }, + { :uuid => 'ffffffff-ffff-ffff-ffff-ffffffffffff', + :from_type => 'gof3r', :to_type => 'pg_restore', num: 8, + :started_at => Time.now, :finished_at => Time.now, + :processed_bytes => 42, :succeeded => true, :warnings => 4}, ] end @@ -276,9 +280,15 @@ module Heroku::Command it "lists successful restores" do stderr, stdout = execute("pg:backups") - expect(stdout).to match(/r003\s*Finished/) + expect(stdout).to match(/r008\s*Completed with 4 warnings/) end + it "lists completed restores with warnings" do + stderr, stdout = execute("pg:backups") + expect(stdout).to match(/r004\s*Failed/) + end + + it "lists failed restores" do stderr, stdout = execute("pg:backups") expect(stdout).to match(/r004\s*Failed/) From d0be78103334e5f85422ce300ab309ade6ebf883 Mon Sep 17 00:00:00 2001 From: tef Date: Mon, 27 Jul 2015 08:39:39 -0700 Subject: [PATCH 645/952] Revise text with Rimas' changes --- lib/heroku/command/pg_backups.rb | 8 ++++---- spec/heroku/command/pg_backups_spec.rb | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index b3f797ad6..6574a87cf 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -151,9 +151,9 @@ def transfer_status(t) if t[:finished_at] && t[:succeeded] warnings = t[:warnings] if warnings && warnings > 0 - "Completed with #{warnings} warnings" #{t[:finished_at]}" + "Finished with #{warnings} warnings" #{t[:finished_at]}" else - "Finished #{t[:finished_at]}" + "Completed #{t[:finished_at]}" end elsif t[:finished_at] && !t[:succeeded] "Failed #{t[:finished_at]}" @@ -291,9 +291,9 @@ def backup_status status = if backup[:succeeded] warnings = backup[:warnings] if warnings && warnings > 0 - "Completed with #{warnings} warnings" + "Finished with #{warnings} warnings" else - "Completed Successfully" + "Completed" end elsif backup[:canceled_at] "Canceled" diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 55bec5c4c..01e378e51 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -265,7 +265,7 @@ module Heroku::Command it "lists successful backups" do stderr, stdout = execute("pg:backups") - expect(stdout).to match(/b001\s*Finished/) + expect(stdout).to match(/b001\s*Completed/) end it "list failed backups" do @@ -275,12 +275,12 @@ module Heroku::Command it "lists old pgbackups" do stderr, stdout = execute("pg:backups") - expect(stdout).to match(/ob047\s*Finished/) + expect(stdout).to match(/ob047\s*Completed/) end it "lists successful restores" do stderr, stdout = execute("pg:backups") - expect(stdout).to match(/r008\s*Completed with 4 warnings/) + expect(stdout).to match(/r008\s*Finished with 4 warnings/) end it "lists completed restores with warnings" do @@ -297,7 +297,7 @@ module Heroku::Command it "lists successful copies" do stderr, stdout = execute("pg:backups") expect(stdout).to match(/===\sCopies/) - expect(stdout).to match(/c005\s*Finished/) + expect(stdout).to match(/c005\s*Completed/) end it "lists failed copies" do @@ -314,7 +314,7 @@ module Heroku::Command Database: #{from_name} Started: #{started_at} Finished: #{finished_at} -Status: Completed Successfully +Status: Completed Type: Manual Original DB Size: #{source_size}.0B Backup Size: #{backup_size}.0B (50% compression) @@ -331,7 +331,7 @@ module Heroku::Command Database: #{from_name} Started: #{started_at} Finished: #{finished_at} -Status: Completed Successfully +Status: Completed Type: Manual Original DB Size: #{source_size}.0B Backup Size: #{backup_size}.0B (50% compression) @@ -348,7 +348,7 @@ module Heroku::Command Database: #{from_name} Started: #{started_at} Finished: #{finished_at} -Status: Completed Successfully +Status: Completed Type: Manual Original DB Size: #{source_size}.0B Backup Size: #{backup_size}.0B (50% compression) @@ -366,7 +366,7 @@ module Heroku::Command === Backup info: b001 Database: #{from_name} Started: #{started_at} -Status: Completed Successfully +Status: Completed Type: Manual Original DB Size: #{source_size}.0B Backup Size: #{backup_size}.0B @@ -385,7 +385,7 @@ module Heroku::Command Database: #{from_name} Started: #{started_at} Finished: #{finished_at} -Status: Completed Successfully +Status: Completed Type: Manual Original DB Size: #{source_size}.0B Backup Size: 0.00B (0% compression) @@ -404,7 +404,7 @@ module Heroku::Command Database: #{from_name} Started: #{started_at} Finished: #{finished_at} -Status: Completed Successfully +Status: Completed Type: Manual Backup Size: #{backup_size}.0B === Backup Logs From 194cfd5245c1e9ce60dde111d09d0a22676cc042 Mon Sep 17 00:00:00 2001 From: Daniel Farina Date: Mon, 27 Jul 2015 21:48:41 -0700 Subject: [PATCH 646/952] Implement automatic SSH tunneling for HPG When Bastion config vars are present, use them to open a tunnel to rewrite the URI of the database to the tunnel. Do this for `pg:psql`, commands like `pg:kill` (which are similar non-interactive variants), and `pg:push` and `pg:pull`. Because the tunneling requires a process to stay alive, switch from "exec"ing psql to "spawn"ing it as a subprocess, but *only* in the case where tunneling is required as a hedge against change regular users using un-tunneled psql. Example use of pg:psql, on an app that has the bastion-requiring database "DATABASE" and the regular database "COPPER": $ heroku config DATABASE_BASTIONS: @ref:blooming-slyly-8913:bastions DATABASE_BASTION_KEY: @ref:blooming-slyly-8913:bastion-key DATABASE_BASTION_REKEYS_AFTER: @ref:blooming-slyly-8913:bastion-rekeys-after DATABASE_URL: @ref:blooming-slyly-8913:url HEROKU_POSTGRESQL_COPPER_URL: @ref:reading-surely-4913:url $ heroku pg:psql -c "select 'hello world'" ---> Connecting to DATABASE_URL ?column? ------------- hello world (1 row) $ heroku pg:psql COPPER -c "select 'hello world'" ---> Connecting to HEROKU_POSTGRESQL_COPPER_URL ?column? ------------- hello world (1 row) Because of the patch to `#exec_sql`, one-shot commands like `pg:kill`, `pg:outliers`, etc work too: $ heroku pg:kill 1234 WARNING: PID 1234 is not a PostgreSQL server process pg_cancel_backend ------------------- f (1 row) $ heroku pg:table-size name | size ------+------ (0 rows) Finally, pg:push and pg:pull work (as apparent to the user) exactly the same as before: $ heroku pg:pull DATABASE test pg_dump: reading schemas pg_dump: reading user-defined tables pg_dump: reading extensions [...] $ heroku pg:push test DATABASE [...] --- Gemfile.lock | 4 ++ heroku.gemspec | 13 ++--- lib/heroku/command/pg.rb | 64 ++++++++++++------------- lib/heroku/helpers/heroku_postgresql.rb | 42 ++++++++++++++++ spec/heroku/command/pg_spec.rb | 31 +++++++++--- 5 files changed, 110 insertions(+), 44 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4e70129e8..56f868e31 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) + net-ssh-gateway (>= 1.2.0) netrc (>= 0.10.0) rest-client (>= 1.6.0) rubyzip (>= 0.9.9) @@ -38,6 +39,9 @@ GEM addressable (~> 2.3) mime-types (1.25.1) multi_json (1.11.0) + net-ssh (2.9.2) + net-ssh-gateway (1.2.0) + net-ssh (>= 2.6.5) netrc (0.10.3) rake (10.4.2) rdoc (4.2.0) diff --git a/heroku.gemspec b/heroku.gemspec index c5a258f6a..38bfbb025 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -21,10 +21,11 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(LICENSE|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", ">= 0.3.19" - gem.add_dependency "launchy", ">= 0.3.2" - gem.add_dependency "netrc", ">= 0.10.0" - gem.add_dependency "rest-client", ">= 1.6.0" - gem.add_dependency "rubyzip", ">= 0.9.9" - gem.add_dependency "multi_json", ">= 1.10" + gem.add_dependency "heroku-api", ">= 0.3.19" + gem.add_dependency "launchy", ">= 0.3.2" + gem.add_dependency "netrc", ">= 0.10.0" + gem.add_dependency "rest-client", ">= 1.6.0" + gem.add_dependency "rubyzip", ">= 0.9.9" + gem.add_dependency "multi_json", ">= 1.10" + gem.add_dependency "net-ssh-gateway", ">= 1.2.0" end diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 909fcbb5c..b269bc449 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -143,7 +143,16 @@ def psql prompt_expr = "#{shorthand}%R%# " prompt_flags = %Q(--set "PROMPT1=#{prompt_expr}" --set "PROMPT2=#{prompt_expr}") puts "---> Connecting to #{attachment.display_name}" - exec "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{set_commands} #{prompt_flags} #{command} #{uri.path[1..-1]}" + attachment.maybe_tunnel do |uri| + command = "psql -U #{uri.user} -h #{uri.host} -p #{uri.port || 5432} #{set_commands} #{prompt_flags} #{command} #{uri.path[1..-1]}" + if attachment.uses_bastion? + spawn(command) + Process.wait + exit($?.exitstatus) + else + exec(command) + end + end rescue Errno::ENOENT output_with_bang "The local psql command could not be located" output_with_bang "For help installing psql, see http://devcenter.heroku.com/articles/local-postgresql" @@ -360,15 +369,16 @@ def push exit(1) end - target_uri = resolve_heroku_url(remote) + target_attachment = resolve_heroku_attachment(remote) source_uri = parse_db_url(local) - pgdr = PgDumpRestore.new( - source_uri, - target_uri, - self) - - pgdr.execute + target_attachment.maybe_tunnel do |uri| + pgdr = PgDumpRestore.new( + source_uri, + uri.to_s, + self) + pgdr.execute + end end # pg:pull @@ -386,15 +396,16 @@ def pull exit(1) end - source_uri = resolve_heroku_url(remote) + source_attachment = resolve_heroku_attachment(remote) target_uri = parse_db_url(local) - pgdr = PgDumpRestore.new( - source_uri, - target_uri, - self) - - pgdr.execute + source_attachment.maybe_tunnel do |uri| + pgdr = PgDumpRestore.new( + uri.to_s, + target_uri, + self) + pgdr.execute + end end @@ -599,8 +610,8 @@ def get_config_var(name) res.data[:body][name] end - def resolve_heroku_url(remote) - generate_resolver.resolve(remote).url + def resolve_heroku_attachment(remote) + generate_resolver.resolve(remote) end def generate_resolver @@ -726,19 +737,6 @@ def wait_for(attach, interval) end end - def find_uri - return @uri if defined? @uri - - attachment = generate_resolver.resolve(shift_argument, "DATABASE_URL") - if attachment.kind_of? Array - uri = URI.parse( attachment.last ) - else - uri = URI.parse( attachment.url ) - end - - @uri = uri - end - def version return @version if defined? @version result = exec_sql("select version();").match(/PostgreSQL (\d+\.\d+\.\d+) on/) @@ -768,8 +766,10 @@ def query_column end def exec_sql(sql) - uri = find_uri - exec_sql_on_uri(sql, uri) + attachment = generate_resolver.resolve(shift_argument, "DATABASE_URL") + attachment.maybe_tunnel do |uri| + exec_sql_on_uri(sql, uri) + end end def exec_sql_on_uri(sql,uri) diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index 40cd1ed63..8da5937da 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -7,6 +7,8 @@ module Heroku::Helpers::HerokuPostgresql class Attachment attr_reader :app, :name, :config_var, :resource_name, :url, :addon, :plan + attr_reader :bastions, :bastion_key + def initialize(raw) @raw = raw @app = raw['app']['name'] @@ -15,6 +17,14 @@ def initialize(raw) @resource_name = raw['resource']['name'] @url = raw['resource']['value'] @addon, @plan = raw['resource']['type'].split(':') + + # Optional Bastion information for tunneling. + if config = raw['config'] + @bastions = if maybe_hosts = config[@name + '_BASTIONS'] + maybe_hosts.split(',') + end + @bastion_key = config[@name + '_BASTION_KEY'] + end end def starter_plan? @@ -32,6 +42,38 @@ def primary_attachment! def primary_attachment? @primary_attachment end + + def uses_bastion? + !!(bastions && bastion_key) + end + + def maybe_tunnel + require "net/ssh/gateway" + + uri = URI.parse(url) + if uses_bastion? + bastion_host = bastions.sample + gateway = Net::SSH::Gateway.new(bastion_host, 'bastion', + paranoid: false, timeout: 15, key_data: [bastion_key]) + begin + local_port = rand(65535 - 49152) + 49152 + gateway.open(uri.host, uri.port, local_port) do |actual_local_port| + uri.host = 'localhost' + uri.port = actual_local_port + yield uri + end + rescue Errno::EADDRINUSE + # Get a new random port if a local binding was not possible. + gateway && gateway.shutdown! + gateway = nil + retry + ensure + gateway && gateway.shutdown! + end + else + yield uri + end + end end def hpg_resolve(identifier, default=nil) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 847760d86..7b9b6c37d 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -345,13 +345,23 @@ module Heroku::Command it 'executes dump restore with correct targets' do pg = Heroku::Command::Pg.new - remote_url = "postgres://someurl.test/#{remote}" + remote_attachment = + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'sushi'}, + 'name' => remote, + 'config_var' => remote + '_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => "postgres://someurl.test/#{remote}", + 'type' => 'heroku-postgresql:ronin'}}) local_url = "postgres:///#{local}" + dump_restore = double() - expect(pg).to receive(:resolve_heroku_url).and_return(remote_url) + expect(pg).to receive(:resolve_heroku_attachment).and_return( + remote_attachment) expect(dump_restore).to receive(:execute) expect(Heroku::Command).to receive(:shift_argument).and_return(local, remote) - expect(PgDumpRestore).to receive(:new).with(local_url, remote_url, pg).and_return(dump_restore) + expect(PgDumpRestore).to receive(:new).with( + local_url, remote_attachment.url, pg).and_return(dump_restore) pg.push end @@ -375,13 +385,22 @@ module Heroku::Command it 'executes dump restore with correct targets' do pg = Heroku::Command::Pg.new - remote_url = "postgres://someurl.test/#{remote}" + remote_attachment = + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'sushi'}, + 'name' => remote, + 'config_var' => remote + '_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => "postgres://someurl.test/#{remote}", + 'type' => 'heroku-postgresql:ronin'}}) local_url = "postgres:///#{local}" dump_restore = double() - expect(pg).to receive(:resolve_heroku_url).and_return(remote_url) + expect(pg).to receive(:resolve_heroku_attachment).and_return( + remote_attachment) expect(dump_restore).to receive(:execute) expect(Heroku::Command).to receive(:shift_argument).and_return(remote, local) - expect(PgDumpRestore).to receive(:new).with(remote_url, local_url, pg).and_return(dump_restore) + expect(PgDumpRestore).to receive(:new).with( + remote_attachment.url, local_url, pg).and_return(dump_restore) pg.pull end From 64fa21187ad2cd3d8241a3f5bfa5bf8eb362a764 Mon Sep 17 00:00:00 2001 From: Brett Goulder Date: Wed, 29 Jul 2015 01:35:01 -0700 Subject: [PATCH 647/952] Update Performance costs --- lib/heroku/command/ps.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 29f93ec55..37927ce15 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -3,7 +3,7 @@ # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - COSTS = {"Free"=>0, "Hobby"=>7, "Standard-1X"=>25, "Standard-2X"=>50, "Performance"=>500, "1X"=>36, "2X"=>72, "PX"=>576} + COSTS = {"Free"=>0, "Hobby"=>7, "Standard-1X"=>25, "Standard-2X"=>50, "Performance"=>250, "Performance"=>500, "Performance-L"=>500, "1X"=>36, "2X"=>72, "PX"=>576} # ps:dynos [QTY] # @@ -449,4 +449,3 @@ def scale_dynos(formation, changes) %w[type restart scale stop].each do |cmd| Heroku::Command::Base.alias_command "dyno:#{cmd}", "ps:#{cmd}" end - From ea1e033771ce790b024a75a23184882b49d0474b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 29 Jul 2015 07:28:40 -0700 Subject: [PATCH 648/952] removed confirm billing call since it no longer exists fixes #1671 --- lib/heroku/command.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 45258c5d1..d27965278 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -213,8 +213,6 @@ def self.run(cmd, arguments=[]) rescue Heroku::API::Errors::Unauthorized, RestClient::Unauthorized => e retry_login = handle_auth_error(e) retry if retry_login - rescue Heroku::API::Errors::VerificationRequired, RestClient::PaymentRequired => e - retry if Heroku::Helpers.confirm_billing rescue Heroku::API::Errors::NotFound => e error extract_error(e.response.body) { e.response.body =~ /^([\w\s]+ not found).?$/ ? $1 : "Resource not found" From a573f0c19a50e448c3ee5072ac746cf5125f86fd Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 29 Jul 2015 14:41:20 -0700 Subject: [PATCH 649/952] v3.40.7 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bcab14098..ab2d9224e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.40.7 2015-07-29 +================= +Automatic SSH tunneling for HPG + 3.40.6 2015-07-23 ================= Fix for space conflicts wrt HEROKU_ORGANIZATION diff --git a/Gemfile.lock b/Gemfile.lock index 56f868e31..1043c43f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.6) + heroku (3.40.7) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index eaaca0580..4ff591d66 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.6" + VERSION = "3.40.7" end From fbae53fb3c3c371d952f371585d4da0b0e4eac17 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 29 Jul 2015 14:41:48 -0700 Subject: [PATCH 650/952] v3.40.7.pre --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1043c43f9..b5a3a22e9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.7) + heroku (3.40.7.pre) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4ff591d66..d73473e53 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.7" + VERSION = "3.40.7.pre" end From c5ef3202f73842fb164630af14cc2fefdaa6fb51 Mon Sep 17 00:00:00 2001 From: Brett Goulder Date: Wed, 29 Jul 2015 14:45:46 -0700 Subject: [PATCH 651/952] Update costs --- lib/heroku/command/ps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 37927ce15..ea9b44c85 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -3,7 +3,7 @@ # manage dynos (dynos, workers) # class Heroku::Command::Ps < Heroku::Command::Base - COSTS = {"Free"=>0, "Hobby"=>7, "Standard-1X"=>25, "Standard-2X"=>50, "Performance"=>250, "Performance"=>500, "Performance-L"=>500, "1X"=>36, "2X"=>72, "PX"=>576} + COSTS = {"Free"=>0, "Hobby"=>7, "Standard-1X"=>25, "Standard-2X"=>50, "Performance-M"=>250, "Performance"=>500, "Performance-L"=>500, "1X"=>36, "2X"=>72, "PX"=>576} # ps:dynos [QTY] # From 59a5fa07b4c1815a6d070b5f45063d0043b93606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Thu, 30 Jul 2015 15:56:19 -0700 Subject: [PATCH 652/952] Use resource name to resolve services. --- lib/heroku/command/pg.rb | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index b269bc449..ab253a5ad 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -554,12 +554,12 @@ def links error("Usage links ") unless [local, remote].all? local_attachment = generate_resolver.resolve(local, "DATABASE_URL") - remote_attachment = resolve_service_or_url(remote) + remote_attachment = resolve_service(remote) output_with_bang("No source database specified.") unless local_attachment output_with_bang("No remote database specified.") unless remote_attachment - response = hpg_client(local_attachment).link_set(remote_attachment.url, options[:as]) + response = hpg_client(local_attachment).link_set(remote_attachment.name, options[:as]) display("New link '#{response[:name]}' successfully created.") when 'destroy' @@ -588,21 +588,13 @@ def links private - def resolve_service_or_url(name_or_url, default=nil) - if name_or_url =~ %r{(postgres://|redis://)} - url = name_or_url - uri = URI.parse(url) - name = url_name(uri) - MaybeAttachment.new(name, url, nil) - else - attachment_name = name_or_url || default - attachment = (resolve_addon(attachment_name) || []).first + def resolve_service(name) + attachment = (resolve_addon(name) || []).first - error("Remote database could not be found.") unless attachment - error("Remote database is invalid.") unless attachment['addon_service']['name'] =~ /heroku-(redis|postgresql)/ + error("Remote database could not be found.") unless attachment + error("Remote database is invalid.") unless attachment['addon_service']['name'] =~ /heroku-(redis|postgresql)/ - MaybeAttachment.new(attachment_name, get_config_var(attachment['config_vars'].first), attachment) - end + MaybeAttachment.new(attachment['name'], nil, attachment) end def get_config_var(name) From a1ddda6b6aedae58913af0a280ce5e3b4926e794 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Mon, 3 Aug 2015 11:29:55 -0700 Subject: [PATCH 653/952] Update RELEASE.md --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index c11243b78..2dc8349c6 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,7 +8,7 @@ This is the normal guide on how to do a release. If you are not a member of the * Run test suite: `bundle exec rake` * Update version number in `lib/heroku/version.rb` to `X.Y.Z` * Bump the patch level `Z` if the release contains bugfixes that do not change functionality - * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality + * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality (bumping minor will also show a message to non-autoupdateable clients that a new version is out) * Run `bundle install` to update the version of heroku in the `Gemfile.lock` * Update `CHANGELOG` * Commit the changes `git commit -m "vX.Y.Z" -a` From 3fecf8f93ea5f8f584c578868ebad9494084ab91 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Mon, 3 Aug 2015 11:33:01 -0700 Subject: [PATCH 654/952] Update RELEASE.md --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 2dc8349c6..546ca4379 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -8,7 +8,7 @@ This is the normal guide on how to do a release. If you are not a member of the * Run test suite: `bundle exec rake` * Update version number in `lib/heroku/version.rb` to `X.Y.Z` * Bump the patch level `Z` if the release contains bugfixes that do not change functionality - * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality (bumping minor will also show a message to non-autoupdateable clients that a new version is out) + * Bump the minor level `Y` if the release contains new functionality or changes to existing functionality (bumping minor will also show a message to non-autoupdateable clients that a new version is out. Save these for either important releases, or features that need to go out.) * Run `bundle install` to update the version of heroku in the `Gemfile.lock` * Update `CHANGELOG` * Commit the changes `git commit -m "vX.Y.Z" -a` From 58317175be94c8b5c82ed5e339bf68797baff68f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 3 Aug 2015 15:53:21 -0700 Subject: [PATCH 655/952] ensure that HEROKU_HEADERS are used for pg:backups commands --- lib/heroku/client/heroku_postgresql.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index c28616474..c636f4773 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -12,6 +12,9 @@ def self.add_headers(headers) end def self.headers + if ENV['HEROKU_HEADERS'] + @headers.merge! Heroku::Helpers.json_decode(ENV['HEROKU_HEADERS']) + end @headers end @@ -37,9 +40,6 @@ def resource_name end def heroku_postgresql_resource - if ENV['HEROKU_HEADERS'] - self.class.add_headers json_decode(ENV['HEROKU_HEADERS']) - end RestClient::Resource.new( "https://#{heroku_postgresql_host}/client/v11/databases", :user => Heroku::Auth.user, From f1d4d45ded33f971d4ae49f992a5f18856dbed7b Mon Sep 17 00:00:00 2001 From: Omar Yasin Date: Tue, 4 Aug 2015 17:39:02 -0700 Subject: [PATCH 656/952] Rename performance-l dynos. --- lib/heroku/command/ps.rb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index ea9b44c85..52e8bd69d 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -140,9 +140,9 @@ def index if type == "run" key = "run: one-off processes" - item = "%s (%s): %s %s: `%s`" % [ process["name"], size, process["state"], since, process["command"] ] + item = "%s (%s): %s %s: `%s`" % [ process["name"], change_performance_l(size), process["state"], since, process["command"] ] else - key = "#{type} (#{size}): `#{process["command"]}`" + key = "#{type} (#{change_performance_l(size)}): `#{process["command"]}`" item = "%s: %s %s" % [ process['name'], process['state'], since ] end @@ -444,6 +444,14 @@ def scale_dynos(formation, changes) ) resp.body.select {|p| changes.any?{|c| c["type"] == p["type"]} } end + + def change_performance_l(size) + if size.downcase == "performance-l" + "Performance" + else + size + end + end end %w[type restart scale stop].each do |cmd| From d0d9f7b4ab46aa5bfe5c1852ccacb5cb8fe98745 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 4 Aug 2015 21:01:26 -0700 Subject: [PATCH 657/952] v3.40.8 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ab2d9224e..2811f3442 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.40.8 2015-08-04 +================= +Update dyno costs +Use resource name for pg services +Fixed bugs with pgbackups + 3.40.7 2015-07-29 ================= Automatic SSH tunneling for HPG diff --git a/Gemfile.lock b/Gemfile.lock index b5a3a22e9..a4b934fd4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.7.pre) + heroku (3.40.8) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d73473e53..eea6ebc39 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.7.pre" + VERSION = "3.40.8" end From 6b43e14694571b6723d3ac5fa9c99f3864e3b639 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 4 Aug 2015 21:44:55 -0700 Subject: [PATCH 658/952] v3.40.9 --- CHANGELOG | 2 +- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 2811f3442..ab4c45f29 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,4 @@ -3.40.8 2015-08-04 +3.40.9 2015-08-04 ================= Update dyno costs Use resource name for pg services diff --git a/Gemfile.lock b/Gemfile.lock index a4b934fd4..6c21f282f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.8) + heroku (3.40.9) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index eea6ebc39..9cc976a41 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.8" + VERSION = "3.40.9" end From 492a76dd5a5187a08f9cb68e0a917faccf23c9a4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 6 Aug 2015 10:31:18 -0700 Subject: [PATCH 659/952] remove auto tier switching this is now down in the API: https://github.com/heroku/api/pull/4587 --- lib/heroku/command/ps.rb | 80 ---------------------------------------- 1 file changed, 80 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 52e8bd69d..2cf6dca04 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -349,87 +349,7 @@ def get_formation ).body end - def change_tier_if_needed(formation, changes) - # first find what tier(s) the user wants to move to - # if there are multiple it will be caught later - new_tiers = tiers_in_formation(changes) - - # no tier specified, so we don't need to change anything - return if new_tiers.length == 0 - - # we have an unknown tier - # instead of validating in the CLI, just continue - # the API will error if there is an issue - return unless new_tiers.all? - - new_tier = new_tiers[0] - current_tier = tiers_in_formation(formation).first - - # the formation has no tier - # change the tier to the requested one - return patch_tier(new_tier) unless current_tier - - # tiers in formation and ps:scale are the same - # just continue - return if new_tier == current_tier - - # is the user changing all the dyno types to the new tier? - if consistent_tier_after_changes?(formation, changes) - # all dyno types are changing to the new tier so we can change to it - patch_tier(new_tier) - else - # some of the proposed changes are in a different tier than existing dyno types - # this is not allowed - from_type = formation[0]["size"] - to_type = changes.map{|c| c["size"]}.compact.first - error("Cannot mix #{to_type} with #{from_type} dynos.\nTo change all dynos to #{to_type}, run `heroku ps:type #{to_type}`.") - end - end - - def tiers_in_formation(formation) - tier_map = { - "free" => "free", - "hobby" => "hobby", - "standard-1x" => "production", - "standard-2x" => "production", - "performance" => "production", - } - formation - .select { |p| p["size"]} # types with a size - .select { |p| p["quantity"] > 0 } # types that have active dynos - .map { |p| p["size"]} # get the size of the type - .map { |s| tier_map[s.downcase]} # get the tier of the size - .uniq - end - - def consistent_tier_after_changes?(formation, changes) - formation = pretend_changes_to_formation(formation, changes) - tiers_in_formation(formation).length == 1 - end - - # return a new formation object with changes applied - # in-memory only, does not hit API - def pretend_changes_to_formation(formation, changes) - formation = deep_clone(formation) - changes.each do |change| - ps = formation.find{|p| p["type"] == change["type"]} - next unless ps - ps["size"] = change["size"] if change["size"] - q = change["quantity"] - if q.is_a? Fixnum - ps["quantity"] = q - elsif q.start_with?('+') - ps["quantity"] += q[1..-1].to_i - elsif q.start_with?('-') - ps["quantity"] -= q[1..-1].to_i - end - end - formation - end - def scale_dynos(formation, changes) - change_tier_if_needed(formation, changes) - # The V3 API supports atomic scale+resize, so we make a raw request here # since the heroku-api gem still only supports V2. resp = api.request( From a34d196aa16d7f3a03a2cd6ee6a61c36982a1e9d Mon Sep 17 00:00:00 2001 From: Brett Goulder Date: Mon, 10 Aug 2015 15:32:16 -0700 Subject: [PATCH 660/952] Revert "Merge pull request #1682 from heroku/omarkj-rename-performance-dynos" This reverts commit ec73163c8ed514b3bc5a21de1530fb0d8e14f3ec, reversing changes made to fd8ad8f3d0f3af0f1a391278b0a17453c558a00c. --- lib/heroku/command/ps.rb | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 52e8bd69d..ea9b44c85 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -140,9 +140,9 @@ def index if type == "run" key = "run: one-off processes" - item = "%s (%s): %s %s: `%s`" % [ process["name"], change_performance_l(size), process["state"], since, process["command"] ] + item = "%s (%s): %s %s: `%s`" % [ process["name"], size, process["state"], since, process["command"] ] else - key = "#{type} (#{change_performance_l(size)}): `#{process["command"]}`" + key = "#{type} (#{size}): `#{process["command"]}`" item = "%s: %s %s" % [ process['name'], process['state'], since ] end @@ -444,14 +444,6 @@ def scale_dynos(formation, changes) ) resp.body.select {|p| changes.any?{|c| c["type"] == p["type"]} } end - - def change_performance_l(size) - if size.downcase == "performance-l" - "Performance" - else - size - end - end end %w[type restart scale stop].each do |cmd| From f9f9bb806cd966109a1372061f755e491a40ea20 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 11 Aug 2015 08:27:01 -0700 Subject: [PATCH 661/952] v3.40.10 --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index ab4c45f29..80d8311a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.40.10 2015-08-11 +================== +Update dyno costs + 3.40.9 2015-08-04 ================= Update dyno costs From b01a62437993d07f769ee02787bd6ea94a11ae1d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 11 Aug 2015 08:27:44 -0700 Subject: [PATCH 662/952] v3.40.10 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 6c21f282f..48338ef71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.9) + heroku (3.40.10) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9cc976a41..9084cf7da 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.9" + VERSION = "3.40.10" end From 06f40e74d0f2150d71bd41d3c53666e3c94d9bf4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 11 Aug 2015 15:20:39 -0700 Subject: [PATCH 663/952] v3.40.11 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 80d8311a9..72750b471 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.40.11 2015-08-11 +================== +Removed auto-tier switching logic + 3.40.10 2015-08-11 ================== Update dyno costs diff --git a/Gemfile.lock b/Gemfile.lock index 48338ef71..389d89c6f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.10) + heroku (3.40.11) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9084cf7da..76169b186 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.10" + VERSION = "3.40.11" end From 66e048c4c15ecb2c293d749807eec3e88b73be3d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 12 Aug 2015 09:14:46 -0700 Subject: [PATCH 664/952] v3.41.0 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 72750b471..f5c384a59 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.41.0 2015-08-12 +================= +Fixed OSX installer for El Capitan +Removed foreman in place of heroku local + 3.40.11 2015-08-11 ================== Removed auto-tier switching logic diff --git a/Gemfile.lock b/Gemfile.lock index 389d89c6f..561f41c51 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.40.11) + heroku (3.41.0) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 76169b186..4c9f26f26 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.40.11" + VERSION = "3.41.0" end From 818d3befe10f4d44dbd5a6a79090a9ec220e6576 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 12 Aug 2015 14:59:00 -0700 Subject: [PATCH 665/952] fix deb releases now that foreman is gone, a directory was missing --- tasks/deb.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/tasks/deb.rake b/tasks/deb.rake index aecaefa37..a163d62d4 100644 --- a/tasks/deb.rake +++ b/tasks/deb.rake @@ -22,6 +22,7 @@ namespace :deb do file dist("heroku-toolbelt-#{version}.apt/heroku-#{version}.deb") => distribution_files("deb") do |t| + mkdir_p File.dirname(t.name) tempdir do mkdir_p "usr/local/heroku" cd "usr/local/heroku" do From da246c35d607cb2125959cba20c6c8db40b2fb93 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 12 Aug 2015 15:05:35 -0700 Subject: [PATCH 666/952] v3.41.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f5c384a59..c8d5f1a65 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.41.1 2015-08-12 +================= +Fix debian releases without foreman + 3.41.0 2015-08-12 ================= Fixed OSX installer for El Capitan diff --git a/Gemfile.lock b/Gemfile.lock index 561f41c51..904fe3860 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.41.0) + heroku (3.41.1) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4c9f26f26..2e859927f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.41.0" + VERSION = "3.41.1" end From ed123e4a4d14ec3425526703d1ea7ad2db291fb0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 12 Aug 2015 18:50:26 -0700 Subject: [PATCH 667/952] fix osx installs without homebrew ensure that /usr/local/bin exists before creating the symlink there --- resources/pkg/postinstall | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/pkg/postinstall b/resources/pkg/postinstall index 88f4bd82e..62fe6377f 100755 --- a/resources/pkg/postinstall +++ b/resources/pkg/postinstall @@ -1,3 +1,4 @@ #!/bin/sh +sudo mkdir -p /usr/local/bin sudo ln -sf /usr/local/heroku/bin/heroku /usr/local/bin/heroku From aac51302fe77ae9edc0dc563a10f136908d223e8 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 12 Aug 2015 18:54:57 -0700 Subject: [PATCH 668/952] v3.41.2 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c8d5f1a65..74d824bf5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.41.2 2015-08-12 +================= +Fix OSX releases without homebrew + 3.41.1 2015-08-12 ================= Fix debian releases without foreman diff --git a/Gemfile.lock b/Gemfile.lock index 904fe3860..7c318702b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.41.1) + heroku (3.41.2) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2e859927f..1cb9f0536 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.41.1" + VERSION = "3.41.2" end From 44953e70b111ace18d835505d052f80320bc6a86 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 12 Aug 2015 19:50:46 -0700 Subject: [PATCH 669/952] v3.41.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 74d824bf5..248122b47 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.41.3 2015-08-12 +================= +Fix incomplete release + 3.41.2 2015-08-12 ================= Fix OSX releases without homebrew diff --git a/Gemfile.lock b/Gemfile.lock index 7c318702b..b0f05bac5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.41.2) + heroku (3.41.3) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1cb9f0536..7f90bf6d5 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.41.2" + VERSION = "3.41.3" end From 8631998f6df0865717869d091b25d424c1edbe9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ali=20Karg=C4=B1n?= Date: Thu, 13 Aug 2015 13:33:16 +0300 Subject: [PATCH 670/952] remove double responde_to check --- lib/heroku/client/rendezvous.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/heroku/client/rendezvous.rb b/lib/heroku/client/rendezvous.rb index 93d6b1acc..a2c75cf86 100644 --- a/lib/heroku/client/rendezvous.rb +++ b/lib/heroku/client/rendezvous.rb @@ -97,9 +97,7 @@ def start def fixup(data) return nil if ! data - if data.respond_to?(:force_encoding) - data.force_encoding('utf-8') if data.respond_to?(:force_encoding) - end + data.force_encoding('utf-8') if data.respond_to?(:force_encoding) if running_on_windows? begin data.gsub!(/\e\[[\d;]+m/, '') From 0953d5dac1aba3b3c837afe852db55c827188b99 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Mon, 17 Aug 2015 12:57:09 -0700 Subject: [PATCH 671/952] Fix `heroku apps --space` to display unjoined apps This changes `heroku apps --space` to use the updated v2 org apps endpoint that now includes the space attribute. Unjoined apps in spaces are now displayed and the rest of the space query and display logic falls inline with orgs now. This also has the side benefit of validating space visibility to improve error messages for unauthorized access. --- lib/heroku/api/spaces_v3_dogwood.rb | 14 ++++++ lib/heroku/command/apps.rb | 44 ++++++++----------- lib/heroku/command/base.rb | 12 ++++- lib/heroku/helpers.rb | 10 +++-- spec/heroku/command/apps_spec.rb | 68 ++++++++++++++++++----------- 5 files changed, 93 insertions(+), 55 deletions(-) create mode 100644 lib/heroku/api/spaces_v3_dogwood.rb diff --git a/lib/heroku/api/spaces_v3_dogwood.rb b/lib/heroku/api/spaces_v3_dogwood.rb new file mode 100644 index 000000000..5a4c25c45 --- /dev/null +++ b/lib/heroku/api/spaces_v3_dogwood.rb @@ -0,0 +1,14 @@ +module Heroku + class API + def get_space_v3_dogwood(space_identity) + request( + :method => :get, + :expects => [200], + :path => "/spaces/#{space_identity}", + :headers => { + "Accept" => "application/vnd.heroku+json; version=3.dogwood" + } + ) + end + end +end diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index b7461ce9b..2eb6363f6 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -28,26 +28,24 @@ class Heroku::Command::Apps < Heroku::Command::Base def index validate_arguments! options[:ignore_no_org] = true - validate_space_xor_org! - apps = if options[:space] - api.get_apps.body.select do |app| - app["space"] && [app["space"]["name"], app["space"]["id"]].include?(options[:space]) - end - elsif org + apps = if org org_api.get_apps(org).body else api.get_apps.body.select { |app| options[:all] ? true : !org?(app["owner_email"]) } end + if options[:space] + apps.select! do |app| + app["space"] && [app["space"]["name"], app["space"]["id"]].include?(options[:space]) + end + end + unless apps.empty? - if options[:space] - styled_header("Apps in space #{options[:space]}") - styled_array(apps.map { |app| regionized_app_name(app) }) - elsif org + if org joined, unjoined = apps.partition { |app| app['joined'] == true } - styled_header("Apps joined in organization #{org}") + styled_header(in_message("Apps joined", app_in_msg_opts)) unless joined.empty? styled_array(joined.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) else @@ -57,7 +55,7 @@ def index end if options[:all] - styled_header("Apps available to join in organization #{org}") + styled_header(in_message("Apps available to join", app_in_msg_opts)) unless unjoined.empty? styled_array(unjoined.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) else @@ -79,10 +77,8 @@ def index end end else - if options[:space] - display("There are no apps available in space #{options[:space]}.") - elsif org - display("There are no apps in organization #{org}.") + if org + display("#{in_message("There are no apps", app_in_msg_opts)}.") else display("You have no apps.") end @@ -265,12 +261,7 @@ def create end begin - display_org = !!org - if info['space'] - space_name = info['space']['name'] - display_org = false - end - action("Creating #{info['name']}", :space => space_name, :org => display_org) do + action("Creating #{info['name']}", app_in_msg_opts) do if info['create_status'] == 'creating' Timeout::timeout(options[:timeout].to_i) do loop do @@ -528,9 +519,12 @@ def region_from_app app region = app["region"].is_a?(Hash) ? app["region"]["name"] : app["region"] end - def validate_space_xor_org! - if options[:space] && options[:org] - error "Specify option for space or org, but not both." + def app_in_msg_opts + display_org = !!org + if options[:space] + space_name = options[:space] + display_org = false end + { :org => display_org, :space => space_name } end end diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index c11f83930..6b2729a5f 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -3,6 +3,7 @@ require "heroku/client/rendezvous" require "heroku/client/organizations" require "heroku/command" +require "heroku/api/spaces_v3_dogwood" class Heroku::Command::Base include Heroku::Helpers @@ -41,7 +42,10 @@ def org @nil = false options[:ignore_no_app] = true - @org ||= if options[:org].is_a?(String) + @org ||= if options[:space].is_a?(String) + validate_space_xor_org! + api.get_space_v3_dogwood(options[:space]).body['organization']['name'] + elsif options[:org].is_a?(String) options[:org] elsif options[:personal] || @nil nil @@ -58,6 +62,12 @@ def org @org end + def validate_space_xor_org! + if options[:space] && options[:org] + error "Specify option for space or org, but not both." + end + end + def api Heroku::Auth.api end diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index d9fbc0a35..f7e8ce032 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -253,9 +253,7 @@ def fail(message) ## DISPLAY HELPERS def action(message, options={}) - message = "#{message} in space #{options[:space]}" if options[:space] - message = "#{message} in organization #{org}" if options[:org] - display("#{message}... ", false) + display("#{in_message(message, options)}... ", false) Heroku::Helpers.error_with_failure = true ret = yield Heroku::Helpers.error_with_failure = false @@ -268,6 +266,12 @@ def action(message, options={}) ret end + def in_message(message, options={}) + message = "#{message} in space #{options[:space]}" if options[:space] + message = "#{message} in organization #{org}" if options[:org] + message + end + def status(message) @status = message end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 3ba442d7f..6e199ee1a 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -6,6 +6,7 @@ module Heroku::Command before(:each) do stub_core + stub_get_space_v3_dogwood stub_organizations ENV.delete('HEROKU_ORGANIZATION') end @@ -185,10 +186,10 @@ module Heroku::Command it "creates app in space" do with_blank_git_repository do - stderr, stdout = execute("apps:create spaceapp --space example-space") + stderr, stdout = execute("apps:create spaceapp --space test-space") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -Creating spaceapp in space example-space... done, stack is cedar-14 +Creating spaceapp in space test-space... done, stack is cedar-14 http://spaceapp.herokuapp.com/ | https://git.heroku.com/spaceapp.git Git remote heroku added STDOUT @@ -292,7 +293,7 @@ module Heroku::Command shared_examples "index with space" do context("and the space has no apps") do before(:each) do - @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }) do { :body => MultiJson.dump([]), :status => 200 @@ -300,52 +301,38 @@ module Heroku::Command end end - after(:each) do - Excon.stubs.delete(@space_apps_stub) - end - it "displays a message when the space has no apps" do stderr, stdout = execute("apps --space test-space") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -There are no apps available in space test-space. +There are no apps in space test-space. STDOUT end end context("and the space has apps") do before(:each) do - @space_apps_stub = Excon.stub({ :method => :get, :path => '/apps' }) do + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }) do { :body => MultiJson.dump([ - {"name" => "space-app-1", "space" => {"id" => "test-space-id", "name" => "test-space"}}, - {"name" => "non-space-app-2", "space" => nil} + { :name => 'space-app-1', :space => {:id => 'test-space-id', :name => 'test-space'}, :joined => true }, + { :name => 'space-app-2', :space => {:id => 'test-space-id', :name => 'test-space'}, :joined => false }, + { :name => 'non-space-app-2', :space => nil, :joined => true } ]), :status => 200 } end end - after(:each) do - Excon.stubs.delete(@space_apps_stub) - end - it "lists only apps in spaces by name" do - stderr, stdout = execute("apps --space test-space") + stderr, stdout = execute("apps --space test-space --all") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -=== Apps in space test-space +=== Apps joined in space test-space space-app-1 -STDOUT - end - - it "lists only apps in spaces by id" do - stderr, stdout = execute("apps --space test-space-id") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== Apps in space test-space-id -space-app-1 +=== Apps available to join in space test-space +space-app-2 STDOUT end @@ -370,6 +357,15 @@ module Heroku::Command end context("index with space and org") do + before(:each) do + Excon.stub({ :method => :get, :path => '/v1/organization/test-org/app' }) do + { + :body => MultiJson.dump([]), + :status => 200 + } + end + end + it "displays error to not specify both" do stderr, stdout = execute("apps --space test-space --org test-org") expect(stdout).to eq("") @@ -545,5 +541,25 @@ module Heroku::Command end end end + + def stub_get_space_v3_dogwood + Excon.stub( + :headers => { 'Accept' => 'application/vnd.heroku+json; version=3.dogwood' }, + :method => :get, + :path => '/spaces/test-space') do + { + :body => { + :created_at => '2015-08-12T19:37:02Z', + :id => '6989c417-304f-4394-b958-f42bc6e1fa4e', + :name => 'test1', + :organization => { + :name => 'test-org' + }, + :state => 'allocated', + :updated_at => '2015-08-12T19:48:07Z' + }.to_json, + } + end + end end end From 161ed0b25b1a504de9715d587712b5966121aaf7 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Tue, 18 Aug 2015 14:46:16 -0700 Subject: [PATCH 672/952] Always display unjoined org apps This changes the org app list to always display joined and unjoined apps together in one list for parity with Dashboard. Hiding and segregating unjoined apps has proved to confuse users. This leaves the --all option in place, but updates the documentation to reflect what is actually does for the personal app list. --- lib/heroku/command/apps.rb | 24 +++--------------------- spec/heroku/command/apps_spec.rb | 22 ++++------------------ 2 files changed, 7 insertions(+), 39 deletions(-) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 2eb6363f6..79bf80853 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -12,7 +12,7 @@ class Heroku::Command::Apps < Heroku::Command::Base # # -o, --org ORG # the org to list the apps for # --space SPACE # HIDDEN: list apps in a given space - # -A, --all # list all apps in the org. Not just joined apps + # -A, --all # list all collaborated apps, including joined org apps in personal app list # -p, --personal # list apps in personal account when a default org is set # #Example: @@ -43,26 +43,8 @@ def index unless apps.empty? if org - joined, unjoined = apps.partition { |app| app['joined'] == true } - - styled_header(in_message("Apps joined", app_in_msg_opts)) - unless joined.empty? - styled_array(joined.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) - else - display("You haven't joined any apps.") - display("Use --all to see unjoined apps.") unless options[:all] - display - end - - if options[:all] - styled_header(in_message("Apps available to join", app_in_msg_opts)) - unless unjoined.empty? - styled_array(unjoined.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) - else - display("There are no apps to join.") - display - end - end + styled_header(in_message("Apps", app_in_msg_opts)) + styled_array(apps.map {|app| regionized_app_name(app) + (app['locked'] ? ' (locked)' : '') }) else my_apps, collaborated_apps = apps.partition { |app| app["owner_email"] == Heroku::Auth.user } diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 6e199ee1a..7df69d0d7 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -263,24 +263,12 @@ module Heroku::Command ) end - it "lists joined apps in an organization" do + it "list all in an organization" do stderr, stdout = execute("apps -o test-org") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -=== Apps joined in organization test-org +=== Apps in organization test-org org-app-1 - -STDOUT - end - - it "list all apps in an organization with the --all flag" do - stderr, stdout = execute("apps --all -o test-org") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== Apps joined in organization test-org -org-app-1 - -=== Apps available to join in organization test-org org-app-2 STDOUT @@ -325,13 +313,11 @@ module Heroku::Command end it "lists only apps in spaces by name" do - stderr, stdout = execute("apps --space test-space --all") + stderr, stdout = execute("apps --space test-space") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT -=== Apps joined in space test-space +=== Apps in space test-space space-app-1 - -=== Apps available to join in space test-space space-app-2 STDOUT From 83b1146b680c0f0d07144326ac2142ddb9a6dc56 Mon Sep 17 00:00:00 2001 From: Jordan Curzon Date: Fri, 3 Jul 2015 02:49:15 -0700 Subject: [PATCH 673/952] support custom kernels --- lib/heroku/command/apps.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 79bf80853..b94ccb78a 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -197,6 +197,7 @@ def info # --ssh-git # Use SSH git protocol # -t, --tier TIER # HIDDEN: the tier for this app # --http-git # HIDDEN: Use HTTP git protocol + # -k, --kernel KERNEL # HIDDEN: Use a custom platform kernel # #Examples: # @@ -231,6 +232,7 @@ def create "region" => options[:region], "space" => options[:space], "stack" => Heroku::Command::Stack::Codex.in(options[:stack]), + "kernel" => options[:kernel], "locked" => options[:locked] } From a6f2e4f6f89259f23f1fd8f0a1c10c18279a9983 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 24 Aug 2015 15:08:27 -0700 Subject: [PATCH 674/952] HEROKU_DEBUG improvments added arrows added request POST body output --- lib/heroku/http_instrumentor.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb index 60e1d4fd6..d8402467a 100644 --- a/lib/heroku/http_instrumentor.rb +++ b/lib/heroku/http_instrumentor.rb @@ -11,17 +11,18 @@ def instrument(name, params={}, &block) when "excon.error" $stderr.puts params[:error].message when "excon.request" - $stderr.print "HTTP #{params[:method].upcase} #{params[:scheme]}://#{params[:host]}#{params[:path]} " + $stderr.print "--> HTTP #{params[:method].upcase} #{params[:scheme]}://#{params[:host]}#{params[:path]} " $stderr.print "[auth] " if headers['Authorization'] && headers['Authorization'] != 'Basic Og==' $stderr.print "[2fa] " if headers['Heroku-Two-Factor-Code'] $stderr.puts filter(params[:query]) + $stderr.puts "--> #{params[:body]}" if params[:body] when "excon.response" - $stderr.puts "#{params[:status]} #{params[:reason_phrase]}" - $stderr.puts "request-id: #{headers['Request-id']}" if headers['Request-Id'] + $stderr.puts "<-- #{params[:status]} #{params[:reason_phrase]}" + $stderr.puts "<-- request-id: #{headers['Request-id']}" if headers['Request-Id'] if headers['Content-Encoding'] == 'gzip' - $stderr.puts filter(ungzip(params[:body])) + $stderr.puts "<-- #{filter(ungzip(params[:body]))}" else - $stderr.puts filter(params[:body]) + $stderr.puts "<-- #{filter(params[:body])}" end else $stderr.puts name From 4b9547285fbbe843f9bde07cb9a46d0334bcdc6a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 24 Aug 2015 15:14:05 -0700 Subject: [PATCH 675/952] v3.41.4 --- CHANGELOG | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 248122b47..d224d4654 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.41.4 2015-08-24 +================= +Always display unjoined org apps +Ensure HEROKU_HEADERS are used for pg:backups commands +HEROKU_DEBUG improvements +Support custom kernels (internal only) + 3.41.3 2015-08-12 ================= Fix incomplete release From b6411b03ed1c19bd3dcd773d0240dddcd526fdab Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 24 Aug 2015 15:14:47 -0700 Subject: [PATCH 676/952] v3.41.4 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b0f05bac5..8b6fe2eb0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.41.3) + heroku (3.41.4) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 7f90bf6d5..83c7ac32b 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.41.3" + VERSION = "3.41.4" end From f5b0283a77fd6193e4c6dc79c93be2e5e0c08eb3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 25 Aug 2015 12:30:00 -0700 Subject: [PATCH 677/952] preauth for pg:backups capture Fixes #1699 --- lib/heroku/command/pg_backups.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 59e8b8ffa..5273027c5 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -360,6 +360,7 @@ def backup_status end def capture_backup + requires_preauth db = shift_argument attachment = generate_resolver.resolve(db, "DATABASE_URL") validate_arguments! From 2bf04675818094f75d6ce54775ceb5421e0bf71f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 25 Aug 2015 17:15:37 -0700 Subject: [PATCH 678/952] windows no longer has crtdll Fixes #1686 Fixes #1696 --- lib/heroku/auth.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 85f8f5d69..a0ef18ee0 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -242,7 +242,7 @@ def ask_for_password_on_windows char = nil password = '' - while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do + while char = Win32API.new("msvcrt", "_getch", [ ], "L").Call do break if char == 10 || char == 13 # received carriage return or newline if char == 127 || char == 8 # backspace and delete password.slice!(-1, 1) From 6419e03e1970d3432ed650e85e36e46b940bb2a7 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Thu, 27 Aug 2015 09:54:16 -0500 Subject: [PATCH 679/952] Created a failing test to reproduce a buildpacks:set bug --- spec/heroku/command/buildpacks_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index 3c202fda1..cfccbd491 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -180,6 +180,22 @@ def stub_get(*buildpacks) end it "adds buildpack URL to the end of list" do + stub_put( + "https://github.com/heroku/heroku-buildpack-java", + "https://github.com/heroku/heroku-buildpack-nodejs", + "https://github.com/heroku/heroku-buildpack-ruby") + stderr, stdout = execute("buildpacks:set -i 3 https://github.com/heroku/heroku-buildpack-ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack set. Next release on example will use: + 1. https://github.com/heroku/heroku-buildpack-java + 2. https://github.com/heroku/heroku-buildpack-nodejs + 3. https://github.com/heroku/heroku-buildpack-ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + + it "adds buildpack URL to the very end of list" do stub_put( "https://github.com/heroku/heroku-buildpack-java", "https://github.com/heroku/heroku-buildpack-nodejs", From 92c961e7d515be354aab29d08a56f1e34cc3b3b7 Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Thu, 27 Aug 2015 09:58:48 -0500 Subject: [PATCH 680/952] Fixed a lt versus lt-or-eq bug in buildpacks:set --- lib/heroku/command/buildpacks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 19502d5b9..1ec0ea43a 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -155,7 +155,7 @@ def mutate_buildpacks_constructive(buildpack_url, index, action) # default behavior if index is out of range, or list is previously empty # is to add buildpack to the list - if app_buildpacks.empty? or index.nil? or app_buildpacks.size < index + if app_buildpacks.empty? or index.nil? or app_buildpacks.size <= index buildpack_urls << buildpack_url end From 5065200cb53d6d344b9b0e70001be67547c962c1 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 27 Aug 2015 12:34:07 -0700 Subject: [PATCH 681/952] updated release server url --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 546ca4379..f6d90eaee 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -13,7 +13,7 @@ This is the normal guide on how to do a release. If you are not a member of the * Update `CHANGELOG` * Commit the changes `git commit -m "vX.Y.Z" -a` * Push changes to master `git push origin master` -* Go to the buildserver and release http://54.148.200.17/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) +* Go to the buildserver and release http://cli-build.herokai.com/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) * [optional] Release the OSX pkg (instructions in [full release guide](./RELEASE-FULL.md)) * [optional] Release the WIN pkg (instructions in [full release guide](./RELEASE-FULL.md)) From 770d6e7c403c38c42456e755f4302523cfd01000 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Fri, 28 Aug 2015 10:26:55 +1000 Subject: [PATCH 682/952] Correctly fallback on service:plan add-on resolution --- lib/heroku/helpers/addons/resolve.rb | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/heroku/helpers/addons/resolve.rb b/lib/heroku/helpers/addons/resolve.rb index 673f6977a..6b685472c 100644 --- a/lib/heroku/helpers/addons/resolve.rb +++ b/lib/heroku/helpers/addons/resolve.rb @@ -10,9 +10,6 @@ module Resolve RESOURCE = /^@?([a-z][a-z0-9-]+)$/ SERVICE_PLAN = /^(?:([a-z0-9_-]+):)?([a-z0-9_-]+)$/ # service / service:plan - class AddonDoesNotExistError < Heroku::API::Errors::Error - end - # Finds attachments that match provided identifier. # # Always returns an Array of 0 or more results. @@ -85,16 +82,23 @@ def resolve_addon(identifier, &filter) if identifier =~ RESOURCE name = $1 - addon = begin - get_addon(name) + begin + addon = get_addon(name) + return [addon] if addon rescue Heroku::API::Errors::Forbidden # treat permission error as no match because there might exist a # resource on someone else's app that has a name which # corresponds to a service name that we wish to check below (e.g. # "memcachier") + rescue Heroku::API::Errors::RequestFailed => e + error_id = (json_decode(e.response.body.to_s) || {})["id"] + if error_id == "multiple_matches" + # ignore; name-based matches are never ambiguous, so this + # identifier is probably a service/plan + else + raise + end end - - return [addon] if addon end if identifier =~ SERVICE_PLAN From b27f40ead7caf9f22eacda70e853dec6cfa5d58a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 28 Aug 2015 16:35:36 -0700 Subject: [PATCH 683/952] do not hide output when non-tty --- lib/heroku/command/certs.rb | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/heroku/command/certs.rb b/lib/heroku/command/certs.rb index 1046089a6..8bd9b89fc 100644 --- a/lib/heroku/command/certs.rb +++ b/lib/heroku/command/certs.rb @@ -226,10 +226,6 @@ def display_warnings(endpoint) end end - def display(msg = "", new_line = true) - super if $stdout.tty? - end - def post_to_ssl_doctor(path, action_text = nil) raise UsageError if args.size < 1 action_text ||= "Resolving trust chain" From a2863e3678e17003e5380bb6d398fb8b6a68db6a Mon Sep 17 00:00:00 2001 From: Greg Burek Date: Fri, 28 Aug 2015 16:39:04 -0700 Subject: [PATCH 684/952] Use same attachment for multiple invocations of exec_sql Running pg:ps will execute two sql statements, one to determine the database version and another to read pg_stat_activity. Two executions of exec_sql will shift the supplied argument out, which leaves the defaul, DATABASE_URL. This commit uses a cached class variable so that multiple exec_sql calls will run on the first supplied database attachment. --- lib/heroku/command/pg.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index ab253a5ad..c1d58c0b6 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -758,8 +758,8 @@ def query_column end def exec_sql(sql) - attachment = generate_resolver.resolve(shift_argument, "DATABASE_URL") - attachment.maybe_tunnel do |uri| + @attachment ||= generate_resolver.resolve(shift_argument, "DATABASE_URL") + @attachment.maybe_tunnel do |uri| exec_sql_on_uri(sql, uri) end end From 6492559e25fedce71fc76ddefe049c436b3a02b6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 31 Aug 2015 16:18:50 -0700 Subject: [PATCH 685/952] use passed in args for v4 takeover --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index afcf1fbbe..dc648aba3 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -15,7 +15,7 @@ def self.try_takeover(command, args) command = commands.find { |t| t["topic"] == topic && (t["command"] == nil || t["default"]) } end return if !command || command["hidden"] - run(command['topic'], command['command'], ARGV[1..-1]) + run(command['topic'], command['command'], args) end def self.load! From c32f23e0186d2f91449c1b79c514004a000dc7d6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 31 Aug 2015 16:37:18 -0700 Subject: [PATCH 686/952] expose find_command function for js takeover --- lib/heroku/jsplugin.rb | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index dc648aba3..036a1661e 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -8,12 +8,7 @@ def self.setup? end def self.try_takeover(command, args) - topic, cmd = command.split(':', 2) - if cmd - command = commands.find { |t| t["topic"] == topic && t["command"] == cmd } - else - command = commands.find { |t| t["topic"] == topic && (t["command"] == nil || t["default"]) } - end + command = find_command(command) return if !command || command["hidden"] run(command['topic'], command['command'], args) end @@ -185,4 +180,13 @@ def self.excon_opts def self.url manifest['builds'][os][arch]['url'] + ".gz" end + + def self.find_command(s) + topic, cmd = s.split(':', 2) + if cmd + commands.find { |t| t["topic"] == topic && t["command"] == cmd } + else + commands.find { |t| t["topic"] == topic && (t["command"] == nil || t["default"]) } + end + end end From 7181205a376db1d42313abaf347fe7b4ec0b5ba1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 31 Aug 2015 16:46:20 -0700 Subject: [PATCH 687/952] v3.41.5 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d224d4654..5f4aa7fc0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.41.5 2015-08-31 +================= +Added preauth to pg:backups capture +Fixed buildpack set with buildpack at end of list +Fix addons:open service plan resolution +Remove extra SQL command for execsql + 3.41.4 2015-08-24 ================= Always display unjoined org apps diff --git a/Gemfile.lock b/Gemfile.lock index 8b6fe2eb0..e5cfadeb5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.41.4) + heroku (3.41.5) heroku-api (>= 0.3.19) launchy (>= 0.3.2) multi_json (>= 1.10) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 83c7ac32b..0ef166d54 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.41.4" + VERSION = "3.41.5" end From d8f59326e21b7e81422ce731d1db88aaf0f868cd Mon Sep 17 00:00:00 2001 From: Amanda Gilmore Date: Fri, 4 Sep 2015 10:25:39 -0700 Subject: [PATCH 688/952] added support for CET and CEST --- lib/heroku/command/pg_backups.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 5273027c5..69e241cdc 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -647,17 +647,19 @@ def parse_schedule_time(time_str) end # do-what-i-mean remapping, since transferatu is (rightfully) picky remap_tzs = { - 'PST' => 'America/Los_Angeles', - 'PDT' => 'America/Los_Angeles', - 'MST' => 'America/Boise', - 'MDT' => 'America/Boise', - 'CST' => 'America/Chicago', - 'CDT' => 'America/Chicago', - 'EST' => 'America/New_York', - 'EDT' => 'America/New_York', - 'Z' => 'UTC', - 'GMT' => 'Europe/London', - 'BST' => 'Europe/London', + 'PST' => 'America/Los_Angeles', + 'PDT' => 'America/Los_Angeles', + 'MST' => 'America/Boise', + 'MDT' => 'America/Boise', + 'CST' => 'America/Chicago', + 'CDT' => 'America/Chicago', + 'EST' => 'America/New_York', + 'EDT' => 'America/New_York', + 'Z' => 'UTC', + 'GMT' => 'Europe/London', + 'BST' => 'Europe/London', + 'CET' => 'Europe/Paris', + 'CEST' => 'Europe/Paris' } if remap_tzs.has_key? tz.upcase tz = remap_tzs[tz.upcase] From 9fd80c05f79d650c1e65716a7a57ae57c3a6d5aa Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Fri, 28 Aug 2015 11:26:35 +1000 Subject: [PATCH 689/952] Lean on API for add-on resolution --- lib/heroku/command/addons.rb | 23 +---- lib/heroku/command/pg.rb | 17 ++-- lib/heroku/helpers/addons/api.rb | 2 +- lib/heroku/helpers/addons/resolve.rb | 139 +++------------------------ 4 files changed, 29 insertions(+), 152 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 153062ede..57eb13551 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -352,26 +352,13 @@ def docs # If it looks like a plan, optimistically open docs, otherwise try to # lookup a corresponding add-on and open the docs for its service. - if identifier.include?(':') - service = identifier.split(':')[0] - launchy("Opening #{service} docs", addon_docs_url(service)) + if identifier.include?(':') || get_service(identifier) + launchy("Opening #{identifier} docs", addon_docs_url(identifier)) else # searching by any number of things - matches = resolve_addon(identifier) - services = matches.map { |m| m['addon_service']['name'] }.uniq - - case services.count - when 0 - # Optimistically open docs for whatever they passed in - launchy("Opening #{identifier} docs", addon_docs_url(identifier)) - when 1 - service = services.first - launchy("Opening #{service} docs", addon_docs_url(service)) - else - error("Multiple add-ons match #{identifier.inspect}.\n" + - "Use the name of one of the add-on resources:\n\n" + - matches.map { |a| "- #{a['name']} (#{a['addon_service']['name']})" }.join("\n")) - end + addon = resolve_addon!(identifier) + service = addon['addon_service']['name'] + launchy("Opening #{service} docs", addon_docs_url(service)) end end diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index c1d58c0b6..c45a25ceb 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -85,8 +85,12 @@ def promote end validate_arguments! - db = db.sub(/_URL$/, '') # allow promoting with a var name - addon = resolve_addon!(db) { |addon| addon['addon_service']['name'] == 'heroku-postgresql' } + addon = resolve_addon!(db) + + if addon['addon_service']['name'] != 'heroku-postgresql' + name = db == addon['name'] ? db : "#{db} (#{addon['name']})" + error("Cannot promote #{name}. It needs to be heroku-postgresql, not #{addon['addon_service']['name']}.") + end promoted_name = 'DATABASE' @@ -589,12 +593,13 @@ def links private def resolve_service(name) - attachment = (resolve_addon(name) || []).first + addon = resolve_addon!(name) - error("Remote database could not be found.") unless attachment - error("Remote database is invalid.") unless attachment['addon_service']['name'] =~ /heroku-(redis|postgresql)/ + error("Remote database is invalid.") unless addon['addon_service']['name'] =~ /heroku-(redis|postgresql)/ - MaybeAttachment.new(attachment['name'], nil, attachment) + MaybeAttachment.new(addon['name'], nil, addon) + rescue Heroku::API::Errors::NotFound + error("Remote database could not be found.") end def get_config_var(name) diff --git a/lib/heroku/helpers/addons/api.rb b/lib/heroku/helpers/addons/api.rb index 4daa35805..e5366e83a 100644 --- a/lib/heroku/helpers/addons/api.rb +++ b/lib/heroku/helpers/addons/api.rb @@ -58,7 +58,7 @@ def get_service!(service) end def get_service(service) - get_service! + get_service!(service) rescue Heroku::API::Errors::NotFound end diff --git a/lib/heroku/helpers/addons/resolve.rb b/lib/heroku/helpers/addons/resolve.rb index 6b685472c..ed83e98d9 100644 --- a/lib/heroku/helpers/addons/resolve.rb +++ b/lib/heroku/helpers/addons/resolve.rb @@ -5,137 +5,22 @@ module Addons module Resolve include Heroku::Helpers::Addons::API - UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i - ATTACHMENT = /^(?:([a-z][a-z0-9-]+)::)?([A-Z][A-Z0-9_]+)$/ - RESOURCE = /^@?([a-z][a-z0-9-]+)$/ - SERVICE_PLAN = /^(?:([a-z0-9_-]+):)?([a-z0-9_-]+)$/ # service / service:plan - - # Finds attachments that match provided identifier. - # - # Always returns an Array of 0 or more results. - def resolve_attachment(identifier, &filter) - results = case identifier - when UUID - [get_attachment(identifier)].compact - when ATTACHMENT - app = $1 || self.app # "app::..." or current app - name = $2 - - attachment = begin - get_attachment(name, :app => app) - rescue Heroku::API::Errors::NotFound - end - - return [attachment] if attachment - - get_attachments(:app => app).select { |att| att["name"][name] } - else - [] - end - - filter ? results.select(&filter) : results + def resolve_addon!(identifier) + if identifier !~ /::/ && (app = maybe_app) + get_addon(identifier, app: app) + end || get_addon!(identifier) end - # Finds a single attachment unambiguously given an identifier. - # - # Returns an attachment hash or exits with an error. - def resolve_attachment!(identifier, &filter) - results = resolve_attachment(identifier, &filter) - - case results.count - when 1 - results[0] - when 0 - error("Can not find attachment with #{identifier.inspect}") - else - app = results.first['app']['name'] - error("Multiple attachments on #{app} match #{identifier.inspect}.\n" + - "Did you mean one of:\n\n" + - results.map { |att| "- #{att['name']}" }.join("\n")) - end + def resolve_attachment!(identifier) + if identifier !~ /::/ && (app = maybe_app) + get_attachment(identifier, app: app) + end || get_attachment!(identifier) end - # Finds add-ons that match provided identifier. - # - # Supports: - # * add-on resource UUID - # * add-on resource name (@my-db / my-db) - # * attachment name (other-app::ATTACHMENT / ATTACHMENT on current app) - # * service name - # * service:plan name - # - # Returns an array in every case except for when using a service name for an - # non-existent add-on. In that case, the error message is returned. - # - def resolve_addon(identifier, &filter) - results = case identifier - when UUID - return [get_addon(identifier)].compact - when ATTACHMENT - # identifier -> Array[Attachment] -> uniq Array[Addon] - matches = resolve_attachment(identifier) - matches. - map { |att| att['addon']['id'] }. - uniq. - map { |addon_id| get_addon(addon_id) } - else # try both resource and service identifiers, because they look similar - if identifier =~ RESOURCE - name = $1 - - begin - addon = get_addon(name) - return [addon] if addon - rescue Heroku::API::Errors::Forbidden - # treat permission error as no match because there might exist a - # resource on someone else's app that has a name which - # corresponds to a service name that we wish to check below (e.g. - # "memcachier") - rescue Heroku::API::Errors::RequestFailed => e - error_id = (json_decode(e.response.body.to_s) || {})["id"] - if error_id == "multiple_matches" - # ignore; name-based matches are never ambiguous, so this - # identifier is probably a service/plan - else - raise - end - end - end - - if identifier =~ SERVICE_PLAN - service_name, plan_name = *[$1, $2].compact - full_plan_name = [service_name, plan_name].join(':') if plan_name - - addons = get_addons(:app => app).select do |addon| - addon['addon_service']['name'] == service_name && # match service - [nil, addon['plan']['name']].include?(full_plan_name) && # match plan, IFF specified - addon['app']['name'] == app # /apps/:id/addons returns un-owned add-ons - end - - return addons - end - - [] - end - - filter ? results.select(&filter) : results - end - - # Finds a single add-on unambiguously given an identifier. - # - # Returns an add-on hash or exits with an error. - def resolve_addon!(identifier, &filter) - results = resolve_addon(identifier, &filter) - - case results.count - when 1 - results[0] - when 0 - error("Can not find add-on with #{identifier.inspect}") - else - error("Multiple add-ons match #{identifier.inspect}.\n" + - "Use the name of add-on resource:\n\n" + - results.map { |a| "- #{a['name']} (#{a['plan']['name']})" }.join("\n")) - end + def maybe_app + app + rescue Heroku::Command::CommandFailed + nil end end end From a7a89c6def0e42eae48a4c5e0923bd1e6c57c74f Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Tue, 8 Sep 2015 12:38:59 +1000 Subject: [PATCH 690/952] Fix some tests --- lib/heroku/command/addons.rb | 1 + spec/heroku/command/addons_spec.rb | 88 +++++++++++++++++++----------- spec/heroku/command/pg_spec.rb | 20 +++---- 3 files changed, 66 insertions(+), 43 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 57eb13551..028284159 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -353,6 +353,7 @@ def docs # If it looks like a plan, optimistically open docs, otherwise try to # lookup a corresponding add-on and open the docs for its service. if identifier.include?(':') || get_service(identifier) + identifier = identifier.split(':').first launchy("Opening #{identifier} docs", addon_docs_url(identifier)) else # searching by any number of things diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index f2d15d4e2..f4f3f838e 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -22,11 +22,11 @@ module Heroku::Command end it "should display no addons when none are configured" do - Excon.stub(method: :get, path: %r(/apps/example/addons)) do + Excon.stub(method: :get, path: '/apps/example/addons') do { body: "[]", status: 200 } end - Excon.stub(method: :get, path: %r(/apps/example/addon-attachments)) do + Excon.stub(method: :get, path: '/apps/example/addon-attachments') do { body: "[]", status: 200 } end @@ -44,7 +44,7 @@ module Heroku::Command end it "should list addons and attachments" do - Excon.stub(method: :get, path: %r(/apps/example/addons)) do + Excon.stub(method: :get, path: '/apps/example/addons') do hooks = build_addon( name: "swimming-nicely-42", plan: { name: "deployhooks:http", price: { cents: 0, unit: "month" }}, @@ -58,7 +58,7 @@ module Heroku::Command { body: MultiJson.encode([hooks, hpg]), status: 200 } end - Excon.stub(method: :get, path: %(/apps/example/addon-attachments)) do + Excon.stub(method: :get, path: '/apps/example/addon-attachments') do hpg = build_attachment( name: "HEROKU_POSTGRESQL_CYAN", addon: { name: "heroku-postgresql-12345", app: { name: "example" }}, @@ -88,7 +88,7 @@ module Heroku::Command describe "list" do before do - Excon.stub(method: :get, path: %r(/addon-services)) do + Excon.stub(method: :get, path: '/addon-services') do services = [ { "name" => "cloudcounter:basic", "state" => "alpha" }, { "name" => "cloudcounter:pro", "state" => "public" }, @@ -107,7 +107,7 @@ module Heroku::Command # TODO: plugin code doesn't support this. Do we need it? xit "sends region option to the server" do - stub_request(:get, %r{/addon-services\?region=eu$}). + stub_request(:get, '/addon-services?region=eu'). to_return(:body => MultiJson.dump([])) execute("addons:list --region=eu") end @@ -141,7 +141,7 @@ module Heroku::Command describe 'v1-style command line params' do before do - Excon.stub(method: :post, path: %r(/apps/example/addons)) do + Excon.stub(method: :post, path: '/apps/example/addons') do { body: MultiJson.encode(addon), status: 201 } end end @@ -162,11 +162,11 @@ module Heroku::Command describe "addons:add" do before do - Excon.stub(method: :get, path: %r{^/apps/example/releases/current}) do + Excon.stub(method: :get, path: '/apps/example/releases/current') do { body: MultiJson.dump({ 'name' => 'v99' }), status: 200 } end - Excon.stub(method: :post, path: %r{apps/example/addons/my_addon$}) do + Excon.stub(method: :post, path: '/apps/example/addons/my_addon') do { body: MultiJson.encode(price: "free"), status: 200 } end end @@ -261,7 +261,7 @@ module Heroku::Command end it "sends the variables to the server" do - Excon.stub(method: :post, path: %r{/apps/example/addons$}) do + Excon.stub(method: :post, path: '/apps/example/addons') do { body: MultiJson.encode(addon), status: 201 } end @@ -318,7 +318,7 @@ module Heroku::Command { :expects => 200, :method => :get, - :path => %r{^/apps/example/releases/current} + :path => '/apps/example/releases/current' }, { :body => MultiJson.dump({ 'name' => 'v99' }), @@ -360,7 +360,7 @@ module Heroku::Command end it "adds an addon with a price" do - Excon.stub(method: :post, path: %r(/apps/example/addons)) do + Excon.stub(method: :post, path: '/apps/example/addons') do addon = build_addon( name: "my_addon", addon_service: { name: "my_addon" }, @@ -377,7 +377,7 @@ module Heroku::Command end it "adds an addon with a price and message" do - Excon.stub(method: :post, path: %r(/apps/example/addons)) do + Excon.stub(method: :post, path: '/apps/example/addons') do addon = build_addon( name: "my_addon", addon_service: { name: "my_addon" }, @@ -400,7 +400,7 @@ module Heroku::Command end it "excludes addon plan from docs message" do - Excon.stub(method: :post, path: %r(/apps/example/addons)) do + Excon.stub(method: :post, path: '/apps/example/addons') do addon = build_addon( name: "my_addon", addon_service: { name: "my_addon" }, @@ -421,7 +421,7 @@ module Heroku::Command end it "adds an addon with a price and multiline message" do - Excon.stub(method: :post, path: %r(/apps/example/addons)) do + Excon.stub(method: :post, path: '/apps/example/addons') do addon = build_addon( name: "my_addon", addon_service: { name: "my_addon" }, @@ -431,7 +431,6 @@ module Heroku::Command { body: MultiJson.encode(addon), status: 201 } end - stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "$200/mo", "message" => "foo\nbar" }) stderr, stdout = execute("addons:create my_addon") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT @@ -454,8 +453,8 @@ module Heroku::Command describe 'upgrading' do let(:addon) do build_addon(name: "my_addon", - app: { name: "example" }, - plan: { name: "my_addon" }) + app: { name: "example" }, + plan: { name: "my_addon" }) end before do @@ -464,7 +463,7 @@ module Heroku::Command { :expects => 200, :method => :get, - :path => %r{^/apps/example/releases/current} + :path => '/apps/example/releases/current' }, { :body => MultiJson.dump({ 'name' => 'v99' }), @@ -510,11 +509,15 @@ module Heroku::Command app: { name: "example" }, price: { cents: 0, unit: "month" }) - Excon.stub(method: :get, path: %r(/apps/example/addons)) do + Excon.stub(method: :get, path: '/apps/example/addons') do { body: MultiJson.encode([my_addon]), status: 200 } end - Excon.stub(method: :patch, path: %r(/apps/example/addons/my_addon)) do + Excon.stub(method: :get, path: '/apps/example/addons/my_service') do + { body: MultiJson.encode(my_addon), status: 200 } + end + + Excon.stub(method: :patch, path: '/apps/example/addons/my_addon') do { body: MultiJson.encode(my_addon), status: 200 } end @@ -539,15 +542,18 @@ module Heroku::Command price: { cents: 0, unit: "month" } ).merge(provision_message: "foo\nbar") - Excon.stub(method: :get, path: %r(/apps/example/addons)) do + Excon.stub(method: :get, path: '/apps/example/addons') do { body: MultiJson.encode([my_addon]), status: 200 } end - Excon.stub(method: :patch, path: %r(/apps/example/addons/my_addon)) do + Excon.stub(method: :get, path: '/apps/example/addons/my_service') do + { body: MultiJson.encode(my_addon), status: 200 } + end + + Excon.stub(method: :patch, path: '/apps/example/addons/my_addon') do { body: MultiJson.encode(my_addon), status: 200 } end - stub_core.install_addon("example", "my_addon", {}).returns({ "price" => "$200/mo", "message" => "foo\nbar" }) stderr, stdout = execute("addons:upgrade my_service") expect(stderr).to eq("") expect(stdout).to eq <<-OUTPUT @@ -575,7 +581,7 @@ module Heroku::Command before do Excon.stub( - { :expects => 200, :method => :get, :path => %r{^/apps/example/releases/current} }, + { :expects => 200, :method => :get, :path => '/apps/example/releases/current' }, { :body => MultiJson.dump({ 'name' => 'v99' }), :status => 200, } ) end @@ -619,11 +625,15 @@ module Heroku::Command addon_service: { name: "my_service" }, app: { name: "example" }) - Excon.stub(method: :get, path: %r(/apps/example/addons)) do + Excon.stub(method: :get, path: '/apps/example/addons') do { body: MultiJson.encode([my_addon]), status: 200 } end - Excon.stub(method: :patch, path: %r(/apps/example/addons/my_service)) do + Excon.stub(method: :patch, path: '/apps/example/addons/my_service') do + { body: MultiJson.encode(my_addon), status: 200 } + end + + Excon.stub(method: :patch, path: '/apps/example/addons/my_addon') do { body: MultiJson.encode(my_addon), status: 200 } end end @@ -719,9 +729,25 @@ module Heroku::Command it "complains when many_per_app" do addon1 = stringify(addon.merge(name: "my_addon1", addon_service: { name: "my_service" })) addon2 = stringify(addon.merge(name: "my_addon2", addon_service: { name: "my_service_2" })) - allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([addon1, addon2]) - stderr, stdout = execute('addons:docs my_service') + Excon.stub(method: :get, path: '/addon-services/thing') do + { status: 404, body:'{}' } + end + Excon.stub(method: :get, path: '/apps/example/addons/thing') do + { status: 404, body:'{}' } + end + + Excon.stub(method: :get, path: '/addons/thing') do + { + status: 422, + body: MultiJson.encode( + id: "multiple_matches", + message: "Ambiguous identifier; multiple matching add-ons found: my_addon1 (my_service), my_addon2 (my_service_2)." + ) + } + end + + stderr, stdout = execute('addons:docs thing') expect(stdout).to eq('') expect(stderr).to eq <<-STDERR ! Multiple add-ons match "my_service". @@ -737,7 +763,7 @@ module Heroku::Command { status: 404 } end - Excon.stub(method: :get, path: %r(/apps/example/addons)) do + Excon.stub(method: :get, path: '/apps/example/addons') do { body: "[]", status: 200 } end @@ -799,7 +825,6 @@ module Heroku::Command end it "complains if no such addon exists" do - allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([]) stderr, stdout = execute('addons:open unknown') expect(stderr).to eq <<-STDERR ! Can not find add-on with "unknown" @@ -808,7 +833,6 @@ module Heroku::Command end it "complains if addon is not installed" do - allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([]) stderr, stdout = execute('addons:open deployhooks:http') expect(stderr).to eq <<-STDOUT ! Can not find add-on with "deployhooks:http" diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 7b9b6c37d..2b64d8b80 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -170,20 +170,18 @@ module Heroku::Command app: { id: 1, name: "example" }, addon: { id: resource[:id], name: "dreaming-ably-42" }) - Excon.stub(method: :get, path: "/addons/#{resource[:id]}") do + Excon.stub(method: :get, path: %r"(/apps/example)?/addons/#{resource[:name]}") do { body: MultiJson.encode(resource), status: 200 } end - Excon.stub(method: :get, path: "/addons/#{resource[:name]}") do - { body: MultiJson.encode(resource), status: 200 } - end - - Excon.stub(method: :get, path: "/apps/example/addon-attachments/HEROKU_POSTGRESQL_RONIN") do - { body: MultiJson.encode(ronin), status: 200 } - end - - Excon.stub(method: :get, path: "/apps/example/addon-attachments/RONIN") do - { body: MultiJson.encode({}), status: 404 } + Excon.stub(method: :get, path: %r"/apps/example/addons/([a-zA-Z0-9_]+)") do |request| + url = ronin[:name] + '_URL' + identifier = request[:captures][:path][0].upcase + if url[identifier] + { body: MultiJson.encode(resource), status: 200 } + else + { body: MultiJson.encode(resource), status: 404 } + end end Excon.stub(method: :get, path: "/apps/example/addon-attachments") do From 4244048411cf8f4b2f04c4a4a499c180d6385378 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 8 Sep 2015 11:58:17 -0700 Subject: [PATCH 691/952] use HEROKU_HEADERS for org requests --- lib/heroku/client/organizations.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 5fdf093a4..4eac30191 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -23,6 +23,9 @@ def add_headers(headers) end def headers + if ENV['HEROKU_HEADERS'] + @headers.merge! Heroku::Helpers.json_decode(ENV['HEROKU_HEADERS']) + end @headers end From 98b3396fd88f47d8a1b1fdd7f952ebd94e8f5007 Mon Sep 17 00:00:00 2001 From: Marty Alchin Date: Thu, 10 Sep 2015 17:14:39 -0700 Subject: [PATCH 692/952] Allow ps:scale to display an existing formation --- lib/heroku/command/ps.rb | 32 ++++++++++++++++++++++++-------- spec/heroku/command/ps_spec.rb | 11 +++++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index bc75930e0..a53b77b07 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -201,17 +201,23 @@ def restart alias_command "restart", "ps:restart" - # ps:scale DYNO1=AMOUNT1 [DYNO2=AMOUNT2 ...] + # ps:scale [DYNO1=AMOUNT1 [DYNO2=AMOUNT2 ...]] # # scale dynos by the given amount # # appending a size (eg. web=2:2X) allows simultaneous scaling and resizing # + # omitting any arguments will display the app's current dyno formation, in a + # format suitable for passing back into ps:scale + # #Examples: # # $ heroku ps:scale web=3:2X worker+1 # Scaling dynos... done, now running web at 3:2X, worker at 1:1X. # + # $ heroku ps:scale + # web=3:2X worker=1:1X + # def scale requires_preauth @@ -224,13 +230,13 @@ def scale end.compact if changes.empty? - error("Usage: heroku ps:scale DYNO1=AMOUNT1[:SIZE] [DYNO2=AMOUNT2 ...]\nMust specify DYNO and AMOUNT to scale.\nDYNO must be alphanumeric.") - end - - action("Scaling dynos") do - new_scales = scale_dynos(get_formation, changes) - .map {|p| "#{p["type"]} at #{p["quantity"]}:#{p["size"]}" } - status("now running " + new_scales.join(", ") + ".") + display_dyno_formation(get_formation) + else + action("Scaling dynos") do + new_scales = scale_dynos(get_formation, changes) + .map {|p| "#{p["type"]} at #{p["quantity"]}:#{p["size"]}" } + status("now running " + new_scales.join(", ") + ".") + end end end @@ -337,6 +343,16 @@ def display_dyno_type_and_costs(formation) end end + def display_dyno_formation(formation) + dynos = formation.sort_by{|d| d['type']}.map{|d| "#{d['type']}=#{d['quantity']}:#{d['size']}"} + + if dynos.empty? + error "No process types on #{app}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile" + else + display dynos.join(" ") + end + end + def get_formation api.request( :expects => 200, diff --git a/spec/heroku/command/ps_spec.rb b/spec/heroku/command/ps_spec.rb index d622fbeb1..ec24aecb9 100644 --- a/spec/heroku/command/ps_spec.rb +++ b/spec/heroku/command/ps_spec.rb @@ -272,6 +272,17 @@ describe "ps:scale" do + it "displays existing dyno formation" do + Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [ + {"quantity" => 1, "size" => "2X", "type" => "web"}, + {"quantity" => 2, "size" => "1X", "type" => "worker"}], status: 200}) + stderr, stdout = execute("ps:scale") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +web=1:2X worker=2:1X +STDOUT + end + it "can scale using key/value format" do Excon.stub({ method: :get, path: "/apps/example/formation" }, { body: [], status: 200}) Excon.stub({ :method => :patch, :path => "/apps/example/formation" }, From 29560c81a1f4eedfe44738a825d5f0deb4eda6aa Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Fri, 11 Sep 2015 15:37:11 +1000 Subject: [PATCH 693/952] Fix broken tests and remove irrelevant tests --- spec/heroku/command/addons_spec.rb | 68 ++++++------------------------ 1 file changed, 12 insertions(+), 56 deletions(-) diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index f4f3f838e..4b3432bc3 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -42,7 +42,7 @@ module Heroku::Command Excon.stubs.shift(2) end - + it "should list addons and attachments" do Excon.stub(method: :get, path: '/apps/example/addons') do hooks = build_addon( @@ -746,32 +746,8 @@ module Heroku::Command ) } end - - stderr, stdout = execute('addons:docs thing') - expect(stdout).to eq('') - expect(stderr).to eq <<-STDERR - ! Multiple add-ons match "my_service". - ! Use the name of one of the add-on resources: - ! - ! - my_addon1 (my_service) - ! - my_addon2 (my_service_2) -STDERR - end - - it "optimistically opens the page if nothing matches" do - Excon.stub(method: :get, path: %r(/addons/unknown)) do - { status: 404 } - end - - Excon.stub(method: :get, path: '/apps/example/addons') do - { body: "[]", status: 200 } - end - - expect(Launchy).to receive(:open).with("https://devcenter.heroku.com/articles/unknown").and_return(Thread.new {}) - stderr, stdout = execute('addons:docs unknown') - expect(stdout).to eq "Opening unknown docs... done\n" - - Excon.stubs.shift(2) + + expect { execute('addons:docs thing') }.to raise_error(Heroku::API::Errors::RequestFailed) end end @@ -796,7 +772,7 @@ module Heroku::Command it "opens the addon if only one matches" do addon.merge!(addon_service: { name: "redistogo:nano" }) - allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([stringify(addon)]) + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon!).and_return(stringify(addon)) require("launchy") expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/#{addon[:id]}").and_return(Thread.new {}) stderr, stdout = execute('addons:open redistogo:nano') @@ -806,38 +782,18 @@ module Heroku::Command STDOUT end - it "complains about ambiguity" do - addon.merge!(addon_service: { name: "deployhooks:email" }) - email = stringify(addon.merge(name: "my_email", plan: { name: "email" })) - http = stringify(addon.merge(name: "my_http", plan: { name: "http" })) - - allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon).and_return([email, http]) - - stderr, stdout = execute('addons:open deployhooks') - expect(stderr).to eq <<-STDERR - ! Multiple add-ons match "deployhooks". - ! Use the name of add-on resource: - ! - ! - my_email (email) - ! - my_http (http) -STDERR - expect(stdout).to eq('') - end - it "complains if no such addon exists" do - stderr, stdout = execute('addons:open unknown') - expect(stderr).to eq <<-STDERR - ! Can not find add-on with "unknown" -STDERR - expect(stdout).to eq('') + Excon.stub(method: :get, path: %r'(/apps/example)?/addons/unknown') do + { status: 404, body:'{"error": "hi"}' } + end + expect { execute('addons:open unknown') }.to raise_error(Heroku::API::Errors::NotFound) end it "complains if addon is not installed" do - stderr, stdout = execute('addons:open deployhooks:http') - expect(stderr).to eq <<-STDOUT - ! Can not find add-on with "deployhooks:http" -STDOUT - expect(stdout).to eq('') + Excon.stub(method: :get, path: %r'(/apps/example)?/addons/deployhooks:http') do + { status: 404, body:'{"error": "hi"}' } + end + expect { execute('addons:open deployhooks:http') }.to raise_error(Heroku::API::Errors::NotFound) end end From b716abdc2ed1c25f3a054fdf4b20b7044ab91195 Mon Sep 17 00:00:00 2001 From: Marty Alchin Date: Fri, 11 Sep 2015 09:06:51 -0700 Subject: [PATCH 694/952] Consolidate the missing Procfile error message --- lib/heroku/command/ps.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index a53b77b07..fa72a28ee 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -325,6 +325,10 @@ def patch_tier(process_tier) ) end + def error_no_process_types + error "No process types on #{app}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile" + end + def display_dyno_type_and_costs(formation) annotated = formation.sort_by{|d| d['type']}.map do |dyno| cost = COSTS[dyno["size"]] @@ -337,7 +341,7 @@ def display_dyno_type_and_costs(formation) end if annotated.empty? - error "No process types on #{app}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile" + error_no_process_types else display_table(annotated, annotated.first.keys, annotated.first.keys) end @@ -347,7 +351,7 @@ def display_dyno_formation(formation) dynos = formation.sort_by{|d| d['type']}.map{|d| "#{d['type']}=#{d['quantity']}:#{d['size']}"} if dynos.empty? - error "No process types on #{app}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile" + error_no_process_types else display dynos.join(" ") end From 0f7858d3414b8b78fa7a64ed040bd228ed607ee9 Mon Sep 17 00:00:00 2001 From: Marty Alchin Date: Fri, 11 Sep 2015 09:07:28 -0700 Subject: [PATCH 695/952] Simplify the ps:scale sorting slightly --- lib/heroku/command/ps.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index fa72a28ee..2e73d9272 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -348,7 +348,7 @@ def display_dyno_type_and_costs(formation) end def display_dyno_formation(formation) - dynos = formation.sort_by{|d| d['type']}.map{|d| "#{d['type']}=#{d['quantity']}:#{d['size']}"} + dynos = formation.map{|d| "#{d['type']}=#{d['quantity']}:#{d['size']}"}.sort if dynos.empty? error_no_process_types From 90a3142dd455917d53d6ac9095e50a49c01f5a31 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 14 Sep 2015 13:31:51 -0700 Subject: [PATCH 696/952] make heroku-accounts warning much more obvious It will not work with http-git or any new commands. It isn't being maintained, but with how new commands work there isn't a way for the plugin to function even if it had a maintainer. --- lib/heroku/auth.rb | 1 - lib/heroku/cli.rb | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 85f8f5d69..ac45c576c 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -267,7 +267,6 @@ def ask_for_password end def ask_for_and_save_credentials - warn "WARNING: heroku-accounts plugin is installed. This plugin is known to have problems with HTTP Git." if defined?(Heroku::Command::Accounts) @credentials = ask_for_credentials debug "Logged in as #{@credentials[0]} with key: #{@credentials[1][0,6]}..." write_credentials diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index ec144dfc6..bc526b95d 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -24,6 +24,7 @@ def self.start(*args) require 'heroku/command' Heroku::Git.check_git_version Heroku::Command.load + warn_if_using_heroku_accounts Heroku::Command.run(command, args) Heroku::Updater.autoupdate rescue Errno::EPIPE => e @@ -40,4 +41,7 @@ def self.start(*args) exit(1) end + def self.warn_if_using_heroku_accounts + warn "WARNING: deprecated ddollar/heroku-accounts plugin is installed." if defined?(Heroku::Command::Accounts) + end end From 81e81d7f79159a93d01547cba136a10f0e4d27af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Tue, 15 Sep 2015 08:50:28 -0700 Subject: [PATCH 697/952] Add missing humanize method. --- lib/heroku/command/pg.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index c1d58c0b6..f45cd7d98 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -588,6 +588,10 @@ def links private + def humanize(key) + key.to_s.gsub(/_/, ' ').split(" ").map(&:capitalize).join(" ") + end + def resolve_service(name) attachment = (resolve_addon(name) || []).first From 8e27098852904fbe88221b5cd985db788afe33b4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 15 Sep 2015 11:27:39 -0700 Subject: [PATCH 698/952] fix domains when partial results are returned --- lib/heroku/api/domains_v3_domain_cname.rb | 14 ++++++++++---- lib/heroku/command/domains.rb | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/heroku/api/domains_v3_domain_cname.rb b/lib/heroku/api/domains_v3_domain_cname.rb index 141dab356..1e0174a35 100644 --- a/lib/heroku/api/domains_v3_domain_cname.rb +++ b/lib/heroku/api/domains_v3_domain_cname.rb @@ -2,15 +2,21 @@ module Heroku class API # TODO: rename methods and filename after 3.domain-cname is merged - def get_domains_v3_domain_cname(app) - request( - :expects => 200, + def get_domains_v3_domain_cname(app, range=nil) + rsp = request( + :expects => [200, 206], :method => :get, :path => "/apps/#{app}/domains", :headers => { - "Accept" => "application/vnd.heroku+json; version=3.domain-cname" + "Accept" => "application/vnd.heroku+json; version=3.domain-cname", + "Range" => range } ) + if rsp.headers['Next-Range'] + rsp.body + get_domains_v3_domain_cname(app, rsp.headers['Next-Range']) + else + rsp.body + end end def post_domains_v3_domain_cname(app, hostname) diff --git a/lib/heroku/command/domains.rb b/lib/heroku/command/domains.rb index 5364fdbea..f2650cbc7 100644 --- a/lib/heroku/command/domains.rb +++ b/lib/heroku/command/domains.rb @@ -24,7 +24,7 @@ class Domains < Base # def index validate_arguments! - domains = api.get_domains_v3_domain_cname(app).body + domains = api.get_domains_v3_domain_cname(app) styled_header("#{app} Heroku Domain") heroku_domain = domains.detect { |d| d['kind'] == 'heroku' || d['kind'] == 'default' } # TODO: remove 'default' after API change From d72928151e42244c783e58634014dfed22e3d7fe Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 15 Sep 2015 16:48:33 -0700 Subject: [PATCH 699/952] instrument orgs --- lib/heroku/client/organizations.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 5fdf093a4..0721b1782 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -12,6 +12,7 @@ def api options = {} auth = "Basic #{Base64.encode64(':' + key).gsub("\n", '')}" hdrs = headers.merge( {"Authorization" => auth } ) options[:ssl_verify_peer] = Heroku::Auth.verify_host?(Heroku::Auth.host) + options[:instrumentor] = HTTPInstrumentor if Heroku::Helpers.debugging? @connection = Excon.new(manager_url, options.merge(:headers => hdrs)) end From c9bd2c20f588b15b5d5b951da9d6811bf0ab1b21 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 15 Sep 2015 16:49:13 -0700 Subject: [PATCH 700/952] add HEROKU_DEBUG_HEADERS flag --- lib/heroku/http_instrumentor.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/heroku/http_instrumentor.rb b/lib/heroku/http_instrumentor.rb index d8402467a..9aad26036 100644 --- a/lib/heroku/http_instrumentor.rb +++ b/lib/heroku/http_instrumentor.rb @@ -15,10 +15,12 @@ def instrument(name, params={}, &block) $stderr.print "[auth] " if headers['Authorization'] && headers['Authorization'] != 'Basic Og==' $stderr.print "[2fa] " if headers['Heroku-Two-Factor-Code'] $stderr.puts filter(params[:query]) + $stderr.puts headers if headers? $stderr.puts "--> #{params[:body]}" if params[:body] when "excon.response" $stderr.puts "<-- #{params[:status]} #{params[:reason_phrase]}" $stderr.puts "<-- request-id: #{headers['Request-id']}" if headers['Request-Id'] + $stderr.puts headers if headers? if headers['Content-Encoding'] == 'gzip' $stderr.puts "<-- #{filter(ungzip(params[:body]))}" else @@ -43,5 +45,9 @@ def filter(obj) end string end + + def headers? + !!ENV['HEROKU_DEBUG_HEADERS'] + end end end From a37f2fec76ab65b7824461590401758771a0d7d0 Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Fri, 18 Sep 2015 09:12:34 -0700 Subject: [PATCH 701/952] Consolidate pg:links output for Attachments+Resources The pg:info command has been updated to display attachments and resources in a consolidated way that makes more sense in a Shareable world. All this really means is not displaying multiple groups of data, if they are for the same logical database as well as displaying the Resource name in addition to Attachment names. Before: === HEROKU_POSTGRESQL_IVORY_URL resource: walking-slowly-1234 === HEROKU_POSTGRESQL_RED_URL resource: walking-slowly-1234 After: === HEROKU_POSTGRESQL_RED_URL, HEROKU_POSTGRESQL_IVORY_URL (walking-slowly-1234) ... --- Gemfile | 1 + Gemfile.lock | 8 +++++ lib/heroku/command/pg.rb | 8 +++-- spec/heroku/command/pg_spec.rb | 57 +++++++++++++++++++++++++++------- 4 files changed, 60 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 2125d3971..70204ecb5 100644 --- a/Gemfile +++ b/Gemfile @@ -12,4 +12,5 @@ group :development, :test do gem "rspec" gem "webmock" gem "coveralls", :require => false + gem "pry" end diff --git a/Gemfile.lock b/Gemfile.lock index e5cfadeb5..e85216d3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,6 +19,7 @@ GEM mime-types xml-simple builder (3.2.2) + coderay (1.1.0) coveralls (0.8.0) multi_json (~> 1.10) rest-client (>= 1.6.8, < 2) @@ -37,12 +38,17 @@ GEM json (1.8.2) launchy (2.4.3) addressable (~> 2.3) + method_source (0.8.2) mime-types (1.25.1) multi_json (1.11.0) net-ssh (2.9.2) net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) netrc (0.10.3) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) rake (10.4.2) rdoc (4.2.0) rest-client (1.6.8) @@ -69,6 +75,7 @@ GEM multi_json (~> 1.0) simplecov-html (~> 0.9.0) simplecov-html (0.9.0) + slop (3.6.0) term-ansicolor (1.3.0) tins (~> 1.0) thor (0.19.1) @@ -88,6 +95,7 @@ DEPENDENCIES heroku! json mime-types + pry rake rr rspec diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index f45cd7d98..6316507af 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -527,13 +527,15 @@ def links dbs = resolver.all_databases.values end + dbs_by_addons = dbs.group_by(&:resource_name) + error("No database attached to this app.") if dbs.compact.empty? - dbs.each_with_index do |attachment, index| - response = hpg_client(attachment).link_list + dbs_by_addons.each_with_index do |(resource, attachments), index| + response = hpg_client(attachments.first).link_list display "\n" if index.nonzero? - styled_header("#{attachment.display_name} (#{attachment.resource_name})") + styled_header("#{attachments.map(&:config_var).join(", ")} (#{resource})") next display response[:message] if response.kind_of?(Hash) next display "No data sources are linked into this database." if response.empty? diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 7b9b6c37d..2f8632753 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -3,16 +3,7 @@ module Heroku::Command describe Pg do - before do - stub_core - - api.post_app "name" => "example" - api.put_config_vars "example", { - "DATABASE_URL" => "postgres://database_url", - "HEROKU_POSTGRESQL_IVORY_URL" => "postgres://database_url", - "HEROKU_POSTGRESQL_RONIN_URL" => "postgres://ronin_database_url" - } - + def stub_attachments(extra_attachments=[]) any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) do |pg| stub(pg).app_attachments.returns([ Heroku::Helpers::HerokuPostgresql::Attachment.new({ @@ -36,10 +27,23 @@ module Heroku::Command 'resource' => {'name' => 'whatever-something-2323', 'value' => 'postgres://follow_database_url', 'type' => 'heroku-postgresql:ronin' }}) - ]) + ].concat(extra_attachments)) end end + before do + stub_core + + api.post_app "name" => "example" + api.put_config_vars "example", { + "DATABASE_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_IVORY_URL" => "postgres://database_url", + "HEROKU_POSTGRESQL_RONIN_URL" => "postgres://ronin_database_url" + } + + stub_attachments + end + after do api.delete_app "example" end @@ -431,5 +435,36 @@ module Heroku::Command expect(parsed_url).to eql url end end + + describe "#links" do + it "returns attachments consolidated by resource" do + stub_pg.link_list.returns([]) + + # API now returns DATABASE as a regular, old, attachment. + # The test setup in this file does not account for that. + # + stub_attachments([ + Heroku::Helpers::HerokuPostgresql::Attachment.new({ + 'app' => {'name' => 'sushi'}, + 'name' => 'DATABASE', + 'config_var' => 'DATABASE_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => 'postgres://database_url', + 'type' => 'heroku-postgresql:ronin' }}) + ]) + + stderr, stdout = execute("pg:links") + expect(stdout).to eq <<-OUTPUT +=== HEROKU_POSTGRESQL_IVORY_URL, DATABASE_URL (loudly-yelling-1232) +No data sources are linked into this database. + +=== HEROKU_POSTGRESQL_RONIN_URL (softly-mocking-123) +No data sources are linked into this database. + +=== HEROKU_POSTGRESQL_FOLLOW_URL (whatever-something-2323) +No data sources are linked into this database. + OUTPUT + end + end end end From 9f47932402dc105ac6a2cab70bcd0cf08ec379f9 Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Fri, 18 Sep 2015 09:28:03 -0700 Subject: [PATCH 702/952] Fix indentation of a few command descriptions --- lib/heroku/command/pg.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 6316507af..e0f40c315 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -411,11 +411,11 @@ def pull # pg:maintenance # - # manage maintenance for - # info # show current maintenance information - # run # start maintenance - # -f, --force # run pg:maintenance without entering application maintenance mode - # window="" # set weekly UTC maintenance window for DATABASE + # manage maintenance for + # info # show current maintenance information + # run # start maintenance + # -f, --force # run pg:maintenance without entering application maintenance mode + # window="" # set weekly UTC maintenance window for DATABASE # # eg: `heroku pg:maintenance window="Sunday 14:30"` def maintenance requires_preauth @@ -501,12 +501,12 @@ def upgrade # pg:links # - # Create links between data stores. Without a subcommand, it lists all - # databases and information on the link. + # Create links between data stores. Without a subcommand, it lists all + # databases and information on the link. # - # create # Create a data link - # --as # override the default link name - # destroy # Destroy a data link between a local and remote database + # create # Create a data link + # --as # override the default link name + # destroy # Destroy a data link between a local and remote database # def links mode = shift_argument || 'list' From a8c4bc020d23afdf6efd5e25c02ce912f3f36820 Mon Sep 17 00:00:00 2001 From: Matte Noble Date: Fri, 18 Sep 2015 09:35:57 -0700 Subject: [PATCH 703/952] Make capitalization consistent across pg command descriptions --- lib/heroku/command/pg.rb | 10 +++++----- lib/heroku/command/pg_backups.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index e0f40c315..868e5ad51 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -42,10 +42,10 @@ def index # pg:info [DATABASE] # - # -x, --extended # Show extended information - # # display database information # + # -x, --extended # Show extended information + # # If DATABASE is not specified, displays all databases # def info @@ -118,10 +118,10 @@ def promote # pg:psql [DATABASE] # - # -c, --command COMMAND # optional SQL command to run - # # open a psql shell to the database # + # -c, --command COMMAND # optional SQL command to run + # # defaults to DATABASE_URL databases if no DATABASE is specified # def psql @@ -501,7 +501,7 @@ def upgrade # pg:links # - # Create links between data stores. Without a subcommand, it lists all + # create links between data stores. Without a subcommand, it lists all # databases and information on the link. # # create # Create a data link diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 69e241cdc..778e99fe9 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -8,7 +8,7 @@ class Heroku::Command::Pg < Heroku::Command::Base # # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) # - # Copy all data from source database to target. At least one of + # copy all data from source database to target. At least one of # these must be a Heroku Postgres database. # def copy @@ -53,7 +53,7 @@ def copy # pg:backups [subcommand] # - # Interact with built-in backups. Without a subcommand, it lists all + # interact with built-in backups. Without a subcommand, it lists all # available backups. The subcommands available are: # # info BACKUP_ID # get information about a specific backup From 01edc0a8b2af8580a890d50eb56069c50cca6bad Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 18 Sep 2015 13:57:38 -0700 Subject: [PATCH 704/952] use HEROKU_FORCE if specified --- lib/heroku/command/addons.rb | 2 +- lib/heroku/command/pg.rb | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 153062ede..7bb06f0be 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -312,7 +312,7 @@ def destroy action("Destroying #{addon['name']} on #{app['name']}") do addon = api.request( :body => json_encode({ - "force" => options[:force], + "force" => options[:force] || ENV['HEROKU_FORCE'] == '1', }), :expects => 200..300, :headers => { diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index f45cd7d98..47e1f15fc 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -322,7 +322,7 @@ def kill output_with_bang "procpid to kill is required" unless procpid && procpid.to_i != 0 procpid = procpid.to_i - cmd = options[:force] ? 'pg_terminate_backend' : 'pg_cancel_backend' + cmd = force? ? 'pg_terminate_backend' : 'pg_cancel_backend' sql = %Q(SELECT #{cmd}(#{procpid});) puts exec_sql(sql) @@ -423,7 +423,7 @@ def maintenance mode, mode_argument = mode_with_argument.split('=') db = shift_argument - no_maintenance = options[:force] + no_maintenance = force? if mode.nil? || db.nil? || !(%w[info run window].include? mode) Heroku::Command.run(current_command, ["--help"]) exit(1) @@ -835,4 +835,8 @@ def find_or_create_non_database_attachment(app) ) end end + + def force? + options[:force] || ENV['HEROKU_FORCE'] == '1' + end end From e444c29c24d3ccbd52d54703e71228ff4fc4521a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 15 Sep 2015 14:42:02 -0700 Subject: [PATCH 705/952] lock down gem versions Fixes #1717 --- Gemfile.lock | 19 +++++++++++-------- heroku.gemspec | 14 +++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e85216d3f..aafc1a33d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,13 +2,13 @@ PATH remote: . specs: heroku (3.41.5) - heroku-api (>= 0.3.19) - launchy (>= 0.3.2) - multi_json (>= 1.10) - net-ssh-gateway (>= 1.2.0) - netrc (>= 0.10.0) - rest-client (>= 1.6.0) - rubyzip (>= 0.9.9) + heroku-api (= 0.3.23) + launchy (= 2.4.3) + multi_json (= 1.11.2) + net-ssh-gateway (= 1.2.0) + netrc (= 0.10.3) + rest-client (= 1.6.8) + rubyzip (= 1.1.7) GEM remote: https://rubygems.org/ @@ -40,7 +40,7 @@ GEM addressable (~> 2.3) method_source (0.8.2) mime-types (1.25.1) - multi_json (1.11.0) + multi_json (1.11.2) net-ssh (2.9.2) net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) @@ -100,3 +100,6 @@ DEPENDENCIES rr rspec webmock + +BUNDLED WITH + 1.10.6 diff --git a/heroku.gemspec b/heroku.gemspec index 38bfbb025..0eae472cc 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -21,11 +21,11 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(LICENSE|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", ">= 0.3.19" - gem.add_dependency "launchy", ">= 0.3.2" - gem.add_dependency "netrc", ">= 0.10.0" - gem.add_dependency "rest-client", ">= 1.6.0" - gem.add_dependency "rubyzip", ">= 0.9.9" - gem.add_dependency "multi_json", ">= 1.10" - gem.add_dependency "net-ssh-gateway", ">= 1.2.0" + gem.add_dependency "heroku-api", "0.3.23" + gem.add_dependency "launchy", "2.4.3" + gem.add_dependency "netrc", "0.10.3" + gem.add_dependency "rest-client", "1.6.8" + gem.add_dependency "rubyzip", "1.1.7" + gem.add_dependency "multi_json", "1.11.2" + gem.add_dependency "net-ssh-gateway", "1.2.0" end From a9e7ef300acc830a229b336b2be406fe3cde5e4b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 21 Sep 2015 11:15:39 -0700 Subject: [PATCH 706/952] v3.42.0 --- CHANGELOG | 16 ++++++++++++++++ lib/heroku/version.rb | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5f4aa7fc0..9c64ff60f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,19 @@ +3.42.0 2015-09-21 +================= +Resolve addons with API +Consolidate addon output +Use HEROKU_FORCE for pg:* commands +Add support for CET and CEST zones +Added v4 2fa shim +Fixed login under windows machines without crtdll +Ensure HEROKU_HEADERS is used with orgs requests +Make warning for using heroku-accounts more admonishing +Display current dyno formation with ps:scale +Added HEROKU_DEBUG_HEADERS flag +Lock gem versions down +Fix domains call with very large number of domains +Removed redis shim + 3.41.5 2015-08-31 ================= Added preauth to pg:backups capture diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0ef166d54..655403125 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.41.5" + VERSION = "3.42.0" end From 09ddc5f2098b3ffeb99b9bfa95a3a400cafcf2da Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 21 Sep 2015 11:21:00 -0700 Subject: [PATCH 707/952] v3.42.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index aafc1a33d..dd6916cc3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.41.5) + heroku (3.42.0) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) From 0dcb7fcffa72160752f596a6b34d219149268556 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 21 Sep 2015 11:26:17 -0700 Subject: [PATCH 708/952] v3.42.1 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9c64ff60f..a03b01683 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.1 2015-09-21 +================= +Re-release of 3.42.0 + 3.42.0 2015-09-21 ================= Resolve addons with API diff --git a/Gemfile.lock b/Gemfile.lock index dd6916cc3..0d7e8f03b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.0) + heroku (3.42.1) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 655403125..77cb1528f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.0" + VERSION = "3.42.1" end From dd2e92041bc81ebad50f8dc5ad79057c09d69bc4 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 21 Sep 2015 12:12:22 -0700 Subject: [PATCH 709/952] correctly check for ddollar/heroku-accounts not heroku/heroku-accounts --- lib/heroku/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index bc526b95d..f03c99457 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -42,6 +42,6 @@ def self.start(*args) end def self.warn_if_using_heroku_accounts - warn "WARNING: deprecated ddollar/heroku-accounts plugin is installed." if defined?(Heroku::Command::Accounts) + warn "WARNING: deprecated ddollar/heroku-accounts plugin is installed." if defined?(Heroku::Command::Accounts.account) end end From dea3b7c8377d7d7660a64d8b7d43dbf0ef68b5a0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 21 Sep 2015 12:16:14 -0700 Subject: [PATCH 710/952] v3.42.2 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a03b01683..dd5153818 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.2 2015-09-21 +================= +Fix heroku-account deprecation warning + 3.42.1 2015-09-21 ================= Re-release of 3.42.0 diff --git a/Gemfile.lock b/Gemfile.lock index 0d7e8f03b..518c53ca1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.1) + heroku (3.42.2) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 77cb1528f..60517234e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.1" + VERSION = "3.42.2" end From ed7cdf7c5ef32ac6431f185dac79cb111bae1c25 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 21 Sep 2015 16:37:47 -0700 Subject: [PATCH 711/952] Fail cleanly when desired formation type does not exist --- lib/heroku/command/ps.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/ps.rb b/lib/heroku/command/ps.rb index 2e73d9272..77df4be61 100644 --- a/lib/heroku/command/ps.rb +++ b/lib/heroku/command/ps.rb @@ -294,8 +294,13 @@ def type changes = if args.any?{|arg| arg =~ /=/} args.map do |arg| if arg =~ /^([a-zA-Z0-9_]+)=([\w-]+)$/ - p = formation.find{|f| f["type"] == $1}.clone - p["size"] = $2 + type, new_size = $1, $2 + current_p = formation.find{|f| f["type"] == type} + if current_p.nil? + error("Type '#{type}' not found in process formation.") + end + p = current_p.clone + p["size"] = new_size p end end.compact From d7150537ad0531c54ac3b11e7b788974f78cb1f7 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Wed, 16 Sep 2015 11:16:12 +1000 Subject: [PATCH 712/952] Use API to resolve attachments for pg:* when no exact match This is a minimal change because the current state of the pg:* commands is quite difficult to work with. I don't have sufficient familiarity with the intended behaviour of each of these commands to make changes with more clarity, but I can roughly see how it should work... That is, most of these commands are still Attachment-based and still APIv2-based. That means we can't easily move to a V3 lookup across the board because of assumptions in behaviour around implicit attachments (DATABASE), access to `config_var` (vs `name`). It leads to some surprising output such as: $ bin/heroku pg:info budding-busily-2230 -a bjeanes ! Ambiguous identifier; multiple matching attachments found: DATABASE, HEROKU_POSTGRESQL_BROWN. In reality, those two ambiguous attachments are the same add-on and pg:info would otherwise show similar information for both. Furthermore, since it's not an attachment name lookup, the `-a` shouldn't be required. Nonetheless, in the common case, look up my multiple forms of add-on and attachment specifiers should work because we just hit the APIv3 for the resolution, then use the `name` of the V3 representation to derive the `config_var` which is used to pluck out the correct record from "all" attachments, as fetched via APIv2. --- lib/heroku/helpers/addons/resolve.rb | 14 ++++++--- lib/heroku/helpers/heroku_postgresql.rb | 11 +++++-- spec/heroku/command/pg_backups_spec.rb | 22 ++++++++++++++ spec/heroku/command/pg_spec.rb | 29 ++++++++++++++---- spec/heroku/helpers/heroku_postgresql_spec.rb | 30 +++++++++++++++++-- 5 files changed, 93 insertions(+), 13 deletions(-) diff --git a/lib/heroku/helpers/addons/resolve.rb b/lib/heroku/helpers/addons/resolve.rb index ed83e98d9..e6666e2ff 100644 --- a/lib/heroku/helpers/addons/resolve.rb +++ b/lib/heroku/helpers/addons/resolve.rb @@ -5,23 +5,29 @@ module Addons module Resolve include Heroku::Helpers::Addons::API - def resolve_addon!(identifier) - if identifier !~ /::/ && (app = maybe_app) + def resolve_addon!(identifier, app=maybe_app) + if identifier !~ /::/ && app get_addon(identifier, app: app) end || get_addon!(identifier) end - def resolve_attachment!(identifier) - if identifier !~ /::/ && (app = maybe_app) + def resolve_attachment!(identifier, app=maybe_app) + if identifier !~ /::/ && app get_attachment(identifier, app: app) end || get_attachment!(identifier) end + private + def maybe_app app rescue Heroku::Command::CommandFailed nil end end + + class Resolver < Struct.new(:api) + include Resolve + end end end diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index 8da5937da..d21f37d58 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -1,4 +1,5 @@ require "heroku/helpers" +require "heroku/helpers/addons/resolve" module Heroku::Helpers::HerokuPostgresql @@ -150,7 +151,6 @@ def hpg_databases } @hpg_databases = Hash[ pairs ] - # TODO: don't bother doing this if DATABASE_URL is already present in hash! if !@hpg_databases.key?('DATABASE_URL') && find_database_url_real_attachment @hpg_databases['DATABASE_URL'] = find_database_url_real_attachment end @@ -189,10 +189,17 @@ def find_database_url_real_attachment end end + def api_resolver + Heroku::Helpers::Addons::Resolver.new(@api) + end + def match_attachments_by_name(name) return [] if name.empty? return [name] if hpg_databases[name] - hpg_databases.keys.grep(%r{#{ name }}i) + att = api_resolver.resolve_attachment!(name, @app_name) + ["#{att['name']}_URL"] + rescue Heroku::API::Errors::NotFound + [] end def hpg_resolve(name, default=nil) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 574a48bd7..a195af881 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -64,6 +64,24 @@ module Heroku::Command "DATABASE_URL" => "postgres://database_url", "HEROKU_POSTGRESQL_TEAL_URL" => teal_url } + + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?|) do |req| + vars = %w[DATABASE_URL HEROKU_POSTGRESQL_GREEN_URL HEROKU_POSTGRESQL_IVORY_URL HEROKU_POSTGRESQL_RED_URL] + identifier = req[:path].scan(%r|[^/]+$|)[0] + matches = vars.grep(Regexp.new(identifier, "i")) + + case matches.size + when 1 + {status: 200, body: MultiJson.encode({ + name: matches[0].gsub(/_URL$/,''), + app: {name: 'example'} + })} + when 0 + {status: 404, body: '{}'} + else + {status: 422, body: '{}' } + end + end end after do @@ -113,6 +131,10 @@ module Heroku::Command end it "copies across apps" do + Excon.stub(method: :get, path: %r|^(/apps/aux-example)?/addon-attachments/(aux-example::)?teal|) do + {status: 200, body: MultiJson.encode({name: 'HEROKU_POSTGRESQL_TEAL'})} + end + stub_pg.pg_copy('TEAL', teal_url, 'RED', red_url).returns(copy_info) stub_pgapp.transfers_get.returns(copy_info) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 2246c714e..4a42ab242 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -7,21 +7,21 @@ def stub_attachments(extra_attachments=[]) any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) do |pg| stub(pg).app_attachments.returns([ Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_IVORY', 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => 'postgres://database_url', 'type' => 'heroku-postgresql:ronin' }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_RONIN', 'config_var' => 'HEROKU_POSTGRESQL_RONIN_URL', 'resource' => {'name' => 'softly-mocking-123', 'value' => 'postgres://ronin_database_url', 'type' => 'heroku-postgresql:ronin' }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_FOLLOW', 'config_var' => 'HEROKU_POSTGRESQL_FOLLOW_URL', 'resource' => {'name' => 'whatever-something-2323', @@ -29,6 +29,13 @@ def stub_attachments(extra_attachments=[]) 'type' => 'heroku-postgresql:ronin' }}) ].concat(extra_attachments)) end + + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?RONIN$|i) do + {status: 200, body: MultiJson.encode({ + name: 'HEROKU_POSTGRESQL_RONIN', + app: {name: 'example'} + })} + end end before do @@ -237,6 +244,12 @@ def stub_attachments(extra_attachments=[]) context "credential resets" do it "resets credentials and promotes to DATABASE_URL if it's the main DB" do + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?iv$|) do + {status: 200, body: MultiJson.encode({ + name: 'HEROKU_POSTGRESQL_IVORY', + app: {name: 'example'} + })} + end stub_pg.rotate_credentials stderr, stdout = execute("pg:credentials iv --reset") expect(stderr).to eq('') @@ -247,6 +260,12 @@ def stub_attachments(extra_attachments=[]) end it "does not update DATABASE_URL if it's not the main db" do + Excon.stub(method: :get, path: %r|^(/apps/example)?/addon-attachments/(example::)?follo$|) do + {status: 200, body: MultiJson.encode({ + name: 'HEROKU_POSTGRESQL_FOLLOW', + app: {name: 'example'} + })} + end stub_pg.rotate_credentials api.put_config_vars "example", { "DATABASE_URL" => "postgres://to_reset_credentials", @@ -349,7 +368,7 @@ def stub_attachments(extra_attachments=[]) pg = Heroku::Command::Pg.new remote_attachment = Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => remote, 'config_var' => remote + '_URL', 'resource' => {'name' => 'loudly-yelling-1232', @@ -389,7 +408,7 @@ def stub_attachments(extra_attachments=[]) pg = Heroku::Command::Pg.new remote_attachment = Heroku::Helpers::HerokuPostgresql::Attachment.new({ - 'app' => {'name' => 'sushi'}, + 'app' => {'name' => 'example'}, 'name' => remote, 'config_var' => remote + '_URL', 'resource' => {'name' => 'loudly-yelling-1232', diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index 3720d3732..ef4cc965c 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -6,11 +6,32 @@ describe Heroku::Helpers::HerokuPostgresql::Resolver do before do - @resolver = described_class.new('appname', double(:api)) + @resolver = described_class.new('appname', api) allow(@resolver).to receive(:app_config_vars) { app_config_vars } allow(@resolver).to receive(:app_attachments) { app_attachments } + + # loosely emulate the API resolution + allow(api).to receive(:request).with(hash_including(method: :get, path: %r|^(/apps/appname)?/addon-attachments/|)) do |req| + identifier = req[:path].scan(%r|[^/]+$|)[0] + + matches = app_config_vars.keys.grep(Regexp.new(identifier, "i")) + + case matches.size + when 1 + Struct.new(:body).new({ + 'name' => matches.first.gsub(/_URL$/,''), + 'app' => { 'name' => 'appname' } + }) + when 0 + raise Heroku::API::Errors::NotFound.new('not found', Struct.new(:body).new({})) + else + raise Heroku::API::Errors::RequestFailed.new('ambiguous', Struct.new(:body).new({})) + end + end end + let(:api) { double(:api) } + let(:app_config_vars) do { "DATABASE_URL" => "postgres://default", @@ -54,7 +75,6 @@ context "when no app is specified or inferred, and identifier does not have app::db shorthand" do it 'exits, complaining about the missing app' do - api = double('api') allow(api).to receive(:get_attachments).and_raise("getting this far will cause an inaccurate 'internal server error' message") no_app_resolver = described_class.new(nil, api) @@ -64,6 +84,12 @@ end context "when the identifier has ::" do + before do + allow(api).to receive(:request).with(hash_including(method: :get, path: %r|^/apps/app2/addon-attachments/black|)) do + Struct.new(:body).new({ 'name' => 'HEROKU_POSTGRESQL_BLACK' }) + end + end + it 'changes the resolver app to the left of the ::' do expect(@resolver.app_name).to eq('appname') att = @resolver.resolve('app2::black') From dee76067bc5f2a65d66f882fa1949253e14e8f21 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 26 Aug 2015 09:56:15 -0700 Subject: [PATCH 713/952] require v4 --- lib/heroku/cli.rb | 3 ++- lib/heroku/command/plugins.rb | 2 +- lib/heroku/command/update.rb | 4 +--- lib/heroku/command/version.rb | 2 +- lib/heroku/jsplugin.rb | 11 ++--------- spec/heroku/command/keys_spec.rb | 1 - spec/heroku/command/version_spec.rb | 1 + spec/heroku/command_spec.rb | 1 + spec/spec_helper.rb | 10 ++++++++++ 9 files changed, 19 insertions(+), 16 deletions(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index f03c99457..3a6091d4d 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -20,7 +20,8 @@ def self.start(*args) $stdout.sync = true if $stdout.isatty Heroku::Updater.warn_if_updating command = args.shift.strip rescue "help" - Heroku::JSPlugin.try_takeover(command, args) if Heroku::JSPlugin.setup? + Heroku::JSPlugin.setup + Heroku::JSPlugin.try_takeover(command, args) require 'heroku/command' Heroku::Git.check_git_version Heroku::Command.load diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 0823159c8..61018b086 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -66,7 +66,7 @@ def uninstall action("Uninstalling #{plugin.name}") do plugin.uninstall end - elsif Heroku::JSPlugin.setup? + else Heroku::JSPlugin.uninstall(plugin.name) end end diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 51bb3e1bb..7c6df9eea 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -17,9 +17,7 @@ class Heroku::Command::Update < Heroku::Command::Base def index validate_arguments! update_from_url(false) - if Heroku::JSPlugin.setup? - Heroku::JSPlugin.update - end + Heroku::JSPlugin.update end # update:beta diff --git a/lib/heroku/command/version.rb b/lib/heroku/command/version.rb index 84a5800c6..12fdd9653 100644 --- a/lib/heroku/command/version.rb +++ b/lib/heroku/command/version.rb @@ -18,7 +18,7 @@ def index validate_arguments! display(Heroku.user_agent) - display(Heroku::JSPlugin.version) if Heroku::JSPlugin.setup? + display(Heroku::JSPlugin.version) Heroku::Command::Plugins.new.index end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 036a1661e..f18a1df47 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -3,10 +3,6 @@ class Heroku::JSPlugin extend Heroku::Helpers - def self.setup? - File.exists? bin - end - def self.try_takeover(command, args) command = find_command(command) return if !command || command["hidden"] @@ -14,7 +10,6 @@ def self.try_takeover(command, args) end def self.load! - return unless setup? this = self topics.each do |topic| Heroku::Command.register_namespace( @@ -47,7 +42,6 @@ def initialize(args, opts) end def self.plugins - return [] unless setup? @plugins ||= `"#{bin}" plugins`.lines.map do |line| name, version = line.split { :name => name, :version => version } @@ -75,12 +69,10 @@ def self.commands end def self.commands_info - copy_ca_cert rescue nil # TODO: remove this once most of the users have the cacert setup @commands_info ||= json_decode(`"#{bin}" commands --json`) end def self.install(name, opts={}) - self.setup system "\"#{bin}\" plugins:install #{name}" if opts[:force] || !self.is_plugin_installed?(name) error "error installing plugin #{name}" if $? != 0 end @@ -107,6 +99,7 @@ def self.bin def self.setup return if File.exist? bin + require 'excon' $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) copy_ca_cert @@ -120,7 +113,7 @@ def self.setup File.delete bin raise 'SHA mismatch for heroku-cli' end - $stderr.puts " done" + $stderr.puts " done.\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" end def self.copy_ca_cert diff --git a/spec/heroku/command/keys_spec.rb b/spec/heroku/command/keys_spec.rb index a5221544a..108d5b35e 100644 --- a/spec/heroku/command/keys_spec.rb +++ b/spec/heroku/command/keys_spec.rb @@ -26,7 +26,6 @@ module Heroku::Command it "adds a key from a specified keyfile path" do # This is because the JSPlugin makes a call to File.exists # Not pretty, but will always work and should be temporary - allow(Heroku::JSPlugin).to receive(:setup?).and_return(false) expect(File).to receive(:exists?).with('.git').and_return(false) expect(File).to receive(:exists?).with('/my/key.pub').and_return(true) expect(File).to receive(:read).with('/my/key.pub').and_return(KEY) diff --git a/spec/heroku/command/version_spec.rb b/spec/heroku/command/version_spec.rb index 28ce58684..b3efa909a 100644 --- a/spec/heroku/command/version_spec.rb +++ b/spec/heroku/command/version_spec.rb @@ -9,6 +9,7 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT #{Heroku.user_agent} +heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5 You have no installed plugins. STDOUT end diff --git a/spec/heroku/command_spec.rb b/spec/heroku/command_spec.rb index d1f0ea38c..fd413769e 100644 --- a/spec/heroku/command_spec.rb +++ b/spec/heroku/command_spec.rb @@ -198,6 +198,7 @@ class Heroku::Command::Test::Multiple; end it "displays the version if --version is used" do expect(heroku("--version")).to eq <<-STDOUT #{Heroku.user_agent} +heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5 You have no installed plugins. STDOUT end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b94aa5d5e..727bd08b8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -240,6 +240,16 @@ module Heroku::Rollbar def self.error(e); end end +require "heroku/jsplugin" +class Heroku::JSPlugin + def self.topics; [] end + def self.commands; [] end + def self.setup; end + def self.run; end + def self.plugins; [] end + def self.version; 'heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5' end +end + require "support/display_message_matcher" require "support/organizations_mock_helper" require "support/addons_helper" From df7a7736d450211a39874ba65acfa7a7697ac467 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 26 Aug 2015 10:41:05 -0700 Subject: [PATCH 714/952] remove shims --- lib/heroku/command/apps.rb | 113 ------------------------------ lib/heroku/command/fork.rb | 42 ----------- lib/heroku/command/git.rb | 51 -------------- lib/heroku/command/local.rb | 29 -------- lib/heroku/command/maintenance.rb | 49 ------------- lib/heroku/command/run.rb | 1 - lib/heroku/command/status.rb | 22 ------ spec/heroku/command/apps_spec.rb | 57 --------------- 8 files changed, 364 deletions(-) delete mode 100644 lib/heroku/command/fork.rb delete mode 100644 lib/heroku/command/git.rb delete mode 100644 lib/heroku/command/local.rb delete mode 100644 lib/heroku/command/maintenance.rb delete mode 100644 lib/heroku/command/status.rb diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index b94ccb78a..49d8e6fcc 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -69,119 +69,6 @@ def index alias_command "list", "apps" - # apps:info - # - # show detailed app information - # - # -s, --shell # output more shell friendly key/value pairs - # - #Examples: - # - # $ heroku apps:info - # === example - # Git URL: https://git.heroku.com/example.git - # Repo Size: 5M - # ... - # - # $ heroku apps:info --shell - # git_url=https://git.heroku.com/example.git - # repo_size=5000000 - # ... - # - def info - validate_arguments! - requires_preauth - app_data = api.get_app(app).body - - unless options[:shell] - styled_header(app_data["name"]) - end - - addons_data = api.get_addons(app).body.map {|addon| addon['name']}.sort rescue {} - collaborators_data = api.get_collaborators(app).body.map {|collaborator| collaborator["email"]}.sort - collaborators_data.reject! {|email| email == app_data["owner_email"]} - - if org? app_data['owner_email'] - app_data['owner'] = app_owner(app_data['owner_email']) - app_data.delete("owner_email") - end - - if options[:shell] - app_data['git_url'] = git_url(app_data['name']) - if app_data['domain_name'] - app_data['domain_name'] = app_data['domain_name']['domain'] - end - unless addons_data.empty? - app_data['addons'] = addons_data.join(',') - end - unless collaborators_data.empty? - app_data['collaborators'] = collaborators_data.join(',') - end - app_data.keys.sort_by { |a| a.to_s }.each do |key| - hputs("#{key}=#{app_data[key]}") - end - else - data = {} - - unless addons_data.empty? - data["Addons"] = addons_data - end - - if app_data["archived_at"] - data["Archived At"] = format_date(app_data["archived_at"]) - end - - data["Collaborators"] = collaborators_data - - if app_data["create_status"] && app_data["create_status"] != "complete" - data["Create Status"] = app_data["create_status"] - end - - if app_data["cron_finished_at"] - data["Cron Finished At"] = format_date(app_data["cron_finished_at"]) - end - - if app_data["cron_next_run"] - data["Cron Next Run"] = format_date(app_data["cron_next_run"]) - end - - if app_data["database_size"] - data["Database Size"] = format_bytes(app_data["database_size"]) - end - - data["Git URL"] = git_url(app_data['name']) - - if app_data["database_tables"] - data["Database Size"].gsub!('(empty)', '0K') + " in #{quantify("table", app_data["database_tables"])}" - end - - if app_data["dyno_hours"].is_a?(Hash) - data["Dyno Hours"] = app_data["dyno_hours"].keys.map do |type| - "%s - %0.2f dyno-hours" % [ type.to_s.capitalize, app_data["dyno_hours"][type] ] - end - end - - data["Owner Email"] = app_data["owner_email"] if app_data["owner_email"] - data["Owner"] = app_data["owner"] if app_data["owner"] - data["Region"] = app_data["region"] if app_data["region"] - data["Space"] = app_data["space"]["name"] if app_data["space"] && app_data["space"]["name"] - data["Repo Size"] = format_bytes(app_data["repo_size"]) if app_data["repo_size"] - data["Slug Size"] = format_bytes(app_data["slug_size"]) if app_data["slug_size"] - data["Cache Size"] = format_bytes(app_data["cache_size"]) if app_data["cache_size"] - - data["Stack"] = Heroku::Command::Stack::Codex.out(app_data["stack"]) - if data["Stack"] != "cedar-10" - data.merge!("Dynos" => app_data["dynos"], "Workers" => app_data["workers"]) - end - - data["Web URL"] = app_data["web_url"] - - styled_hash(data) - end - end - - alias_command "info", "apps:info" - # apps:create [NAME] # # create a new app diff --git a/lib/heroku/command/fork.rb b/lib/heroku/command/fork.rb deleted file mode 100644 index 11f209312..000000000 --- a/lib/heroku/command/fork.rb +++ /dev/null @@ -1,42 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # clone an existing app - # - class Fork < Base - - # fork - # - # --from FROM # app to fork from - # --to TO # app to create - # -s, --stack STACK # specify a stack for the new app - # --region REGION # specify a region - # --skip-pg # skip postgres databases - # - # Copy config vars and Heroku Postgres data, and re-provision add-ons to a new app. - # New app name should not be an existing app. The new app will be created as part of the forking process. - # - #Example: - # - # $ heroku fork --from my-production-app --to my-development-app - # Forking my-production-app... done. Forked to my-development-app - # Deploying 60a8b0f to my-development-app... done - # Adding addon memcachier:dev to my-development-app... done - # Adding addon heroku-postgresql:hobby-dev to my-development-app... done - # Transferring HEROKU_POSTGRESQL_AMBER to DATABASE... - # Progress: done - # Copying config vars: - # LANG - # RAILS_ENV - # RACK_ENV - # SECRET_KEY_BASE - # RAILS_SERVE_STATIC_FILES - # ... done - # Fork complete. View it at https://my-development-app.herokuapp.com/ - def index - Heroku::JSPlugin.install('heroku-fork') - Heroku::JSPlugin.run('fork', nil, ARGV[1..-1]) - end - end -end diff --git a/lib/heroku/command/git.rb b/lib/heroku/command/git.rb deleted file mode 100644 index 2ccfb3b77..000000000 --- a/lib/heroku/command/git.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "heroku/command/base" - -# manage git for apps -# -class Heroku::Command::Git < Heroku::Command::Base - - # git:clone [DIRECTORY] - # - # clones a heroku app to your local machine at DIRECTORY (defaults to app name) - # - # -a, --app APP # the Heroku app to use - # -r, --remote REMOTE # the git remote to create, default "heroku" - # --ssh-git # use SSH git protocol - # --http-git # HIDDEN: Use HTTP git protocol - # - # - #Examples: - # - # $ heroku git:clone -a example - # Cloning into 'example'... - # remote: Counting objects: 42, done. - # ... - # - def clone - Heroku::JSPlugin.install('heroku-git') - Heroku::JSPlugin.run('git', 'clone', ARGV[1..-1]) - end - - alias_command "clone", "git:clone" - - # git:remote [OPTIONS] - # - # adds a git remote to an app repo - # - # if OPTIONS are specified they will be passed to git remote add - # - # -a, --app APP # the Heroku app to use - # -r, --remote REMOTE # the git remote to create, default "heroku" - # --ssh-git # use SSH git protocol - # --http-git # HIDDEN: Use HTTP git protocol - # - #Examples: - # - # $ heroku git:remote -a example - # set git remote heroku to https://git.heroku.com/example.git - # - def remote - Heroku::JSPlugin.install('heroku-git') - Heroku::JSPlugin.run('git', 'remote', ARGV[1..-1]) - end -end diff --git a/lib/heroku/command/local.rb b/lib/heroku/command/local.rb deleted file mode 100644 index 19fb6a9ea..000000000 --- a/lib/heroku/command/local.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # run heroku app locally - class Local < Base - - # Usage: heroku local [PROCESSNAME] - # - # -f, --procfile PROCFILE # use a different Procfile - # -e, --env ENV # location of env file (defaults to .env) - # -c, --concurrency CONCURRENCY # number of processes to start - # -p, --port PORT # port to listen on - # -r, --restart # restart process if it dies - # - # Start the application specified by a Procfile (defaults to ./Procfile) - # - # Examples: - # - # heroku local - # heroku local web - # heroku local -f Procfile.test -e .env.test - # - def index - Heroku::JSPlugin.install('heroku-local') - Heroku::JSPlugin.run('local', nil, ARGV[1..-1]) - end - end -end diff --git a/lib/heroku/command/maintenance.rb b/lib/heroku/command/maintenance.rb deleted file mode 100644 index e264f1dce..000000000 --- a/lib/heroku/command/maintenance.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "heroku/command/base" - -# manage maintenance mode for an app -# -class Heroku::Command::Maintenance < Heroku::Command::Base - - # maintenance - # - # display the current maintenance status of app - # - #Example: - # - # $ heroku maintenance - # off - # - def index - Heroku::JSPlugin.install('heroku-apps') - Heroku::JSPlugin.run('maintenance', nil, ARGV[1..-1]) - end - - # maintenance:on - # - # put the app into maintenance mode - # - #Example: - # - # $ heroku maintenance:on - # Enabling maintenance mode for example - # - def on - Heroku::JSPlugin.install('heroku-apps') - Heroku::JSPlugin.run('maintenance', 'on', ARGV[1..-1]) - end - - # maintenance:off - # - # take the app out of maintenance mode - # - #Example: - # - # $ heroku maintenance:off - # Disabling maintenance mode for example - # - def off - Heroku::JSPlugin.install('heroku-apps') - Heroku::JSPlugin.run('maintenance', 'off', ARGV[1..-1]) - end - -end diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index fffa19d40..24d23d493 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -40,7 +40,6 @@ class Heroku::Command::Run < Heroku::Command::Base # def index if ARGV.include?('--') || ARGV.include?('--exit-code') - Heroku::JSPlugin.install('heroku-run') Heroku::JSPlugin.run('run', nil, ARGV[1..-1]) return end diff --git a/lib/heroku/command/status.rb b/lib/heroku/command/status.rb deleted file mode 100644 index 993d6fa5f..000000000 --- a/lib/heroku/command/status.rb +++ /dev/null @@ -1,22 +0,0 @@ -require "heroku/command/base" - -# check status of heroku platform -# -class Heroku::Command::Status < Heroku::Command::Base - - # status - # - # display current status of heroku platform - # - #Example: - # - # $ heroku status - # === Heroku Status - # Development: No known issues at this time. - # Production: No known issues at this time. - # - def index - Heroku::JSPlugin.install('heroku-status') - Heroku::JSPlugin.run('status', nil, ARGV[1..-1]) - end -end diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 7df69d0d7..b3aaf4ade 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -11,63 +11,6 @@ module Heroku::Command ENV.delete('HEROKU_ORGANIZATION') end - context("info") do - - before(:each) do - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "displays implicit app info" do - stderr, stdout = execute("apps:info") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== example -Git URL: https://git.heroku.com/example.git -Owner Email: email@example.com -Stack: cedar-10 -Web URL: http://example.herokuapp.com/ -STDOUT - end - - it "gets explicit app from --app" do - stderr, stdout = execute("apps:info --app example") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== example -Git URL: https://git.heroku.com/example.git -Owner Email: email@example.com -Stack: cedar-10 -Web URL: http://example.herokuapp.com/ -STDOUT - end - - it "shows shell app info when --shell option is used" do - stderr, stdout = execute("apps:info --shell") - expect(stderr).to eq("") - expect(stdout).to match Regexp.new(<<-STDOUT) -create_status=complete -created_at=\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} [+-]\\d{4} -dynos=0 -git_url=https://git.heroku.com/example.git -id=\\d{1,5} -name=example -owner_email=email@example.com -repo_migrate_status=complete -repo_size= -requested_stack= -slug_size= -stack=cedar -web_url=http://example.herokuapp.com/ -workers=0 -STDOUT - end - - end - context("create") do it "without a name" do From d3b342fd69e91d6eaeeae8f8040d1ec66907c67c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 17 Sep 2015 17:21:09 -0700 Subject: [PATCH 715/952] use cli-assets.heroku.com host --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index f18a1df47..04c4cf735 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -158,7 +158,7 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("https://d1gvo455cekpjp.cloudfront.net/master/manifest.json", excon_opts).body) + @manifest ||= JSON.parse(Excon.get("https://cli-assets.heroku.com/master/manifest.json", excon_opts).body) end def self.excon_opts From 16d42244ae2766e2f7ecb820d6515a166a052758 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 21 Sep 2015 11:04:52 -0700 Subject: [PATCH 716/952] remove two_factor shim --- lib/heroku/command/two_factor.rb | 41 -------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 lib/heroku/command/two_factor.rb diff --git a/lib/heroku/command/two_factor.rb b/lib/heroku/command/two_factor.rb deleted file mode 100644 index fcbb6bceb..000000000 --- a/lib/heroku/command/two_factor.rb +++ /dev/null @@ -1,41 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - # manage two-factor authentication settings - # - class TwoFactor < BaseWithApp - # twofactor - # - # Display whether two-factor authentication is enabled or not - # - def index - Heroku::JSPlugin.setup - Heroku::JSPlugin.run('twofactor', nil, ARGV[1..-1]) - end - - alias_command "2fa", "twofactor" - - # twofactor:disable - # - # Disable two-factor authentication for your account - # - def disable - Heroku::JSPlugin.setup - Heroku::JSPlugin.run('twofactor', 'disable', ARGV[1..-1]) - end - - alias_command "2fa:disable", "twofactor:disable" - - - # twofactor:generate-recovery-codes - # - # Generates and replaces recovery codes - # - def generate_recovery_codes - Heroku::JSPlugin.setup - Heroku::JSPlugin.run('twofactor', 'generate-recovery-codes', ARGV[1..-1]) - end - - alias_command "2fa:generate-recovery-codes", "twofactor:generate_recovery_codes" - end -end From fe68e790c2384a5b7fa64fc01c41dfa2b3ca2815 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 22 Sep 2015 11:51:08 -0700 Subject: [PATCH 717/952] v3.42.3 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dd5153818..90b48c022 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.3 2015-09-22 +================= +Require v4 CLI for all commands + 3.42.2 2015-09-21 ================= Fix heroku-account deprecation warning diff --git a/Gemfile.lock b/Gemfile.lock index 518c53ca1..3bb1f29ef 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.2) + heroku (3.42.3) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 60517234e..1e4deefa5 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.2" + VERSION = "3.42.3" end From b22d392acbd32bcaa5dc5b9731b77ca2da36b5bf Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 23 Sep 2015 09:36:32 -0700 Subject: [PATCH 718/952] auto-remove heroku-accounts --- lib/heroku/cli.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 3a6091d4d..a5db37b52 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -43,6 +43,10 @@ def self.start(*args) end def self.warn_if_using_heroku_accounts - warn "WARNING: deprecated ddollar/heroku-accounts plugin is installed." if defined?(Heroku::Command::Accounts.account) + if defined?(Heroku::Command::Accounts.account) + $stderr.print "Uninstalling deprecated ddollar/heroku-accounts plugin..." + Heroku::Plugin.new('heroku-accounts').uninstall + $stderr.puts " done" + end end end From 78f65f3e8d90797bd2f15b87ad0115ca1296c831 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 23 Sep 2015 20:44:40 -0700 Subject: [PATCH 719/952] update v4 before attempting to update v3 --- lib/heroku/command/update.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index 7c6df9eea..e89bfa27f 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -16,8 +16,8 @@ class Heroku::Command::Update < Heroku::Command::Base # def index validate_arguments! - update_from_url(false) Heroku::JSPlugin.update + update_from_url(false) end # update:beta From 524f9e795e8530bdf740190c5fdfc34a81ff27b3 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 25 Sep 2015 06:56:16 -0700 Subject: [PATCH 720/952] use LOCALAPPDIR on windows --- lib/heroku/jsplugin.rb | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 04c4cf735..2cc5fbcc7 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -89,14 +89,18 @@ def self.version `"#{bin}" version` end - def self.bin - if os == 'windows' - File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli.exe") + def self.app_dir + if os == 'windows' && ENV['LOCALAPPDIR'] + File.join(ENV['LOCALAPPDIR'], 'heroku') else - File.join(Heroku::Helpers.home_directory, ".heroku", "heroku-cli") + File.join(Heroku::Helpers.home_directory, '.heroku') end end + def self.bin + File.join(app_dir, ".heroku", os == 'windows' ? 'heroku-cli.exe' : 'heroku-cli') + end + def self.setup return if File.exist? bin require 'excon' @@ -117,7 +121,7 @@ def self.setup end def self.copy_ca_cert - to = File.join(Heroku::Helpers.home_directory, ".heroku", "cacert.pem") + to = File.join(app_dir, "cacert.pem") return if File.exists?(to) from = File.expand_path("../../../data/cacert.pem", __FILE__) FileUtils.copy(from, to) From 0b174e5a72d027c139bc95ebc463268f4f71c6a0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 25 Sep 2015 11:21:54 -0700 Subject: [PATCH 721/952] fix non-ascii home directory on windows --- lib/heroku/helpers.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index f7e8ce032..ce14ceaf6 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -7,8 +7,7 @@ module Helpers def home_directory if running_on_windows? && RUBY_VERSION == '1.9.3' - # https://bugs.ruby-lang.org/issues/10126 - Dir.home.force_encoding('cp775') + File.expand_path('~') else Dir.home end From 380c4bacd6e322a8fa1751af6394ba8d343510ec Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 25 Sep 2015 11:32:27 -0700 Subject: [PATCH 722/952] fix cygwin exec --- resources/exe/heroku | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/resources/exe/heroku b/resources/exe/heroku index 7a7fd4ce5..5b7dc9c2d 100755 --- a/resources/exe/heroku +++ b/resources/exe/heroku @@ -1,7 +1,5 @@ #!/bin/sh -# find embedded ruby relative to script -bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` -exec "$bindir/ruby" -x "$0" "$@" +exec "heroku.bat" #!/usr/bin/env ruby # encoding: UTF-8 From 38b8a92c665d21bfa077b2c7f764db400c31076c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 25 Sep 2015 10:14:00 -0700 Subject: [PATCH 723/952] never use v4 for help --- lib/heroku/jsplugin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 04c4cf735..9466e648a 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -4,9 +4,10 @@ class Heroku::JSPlugin extend Heroku::Helpers def self.try_takeover(command, args) + return if command == 'help' || args.include?('--help') command = find_command(command) return if !command || command["hidden"] - run(command['topic'], command['command'], args) + run(ARGV[0], nil, ARGV[1..-1]) end def self.load! @@ -125,7 +126,6 @@ def self.copy_ca_cert def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic - debug("running #{cmd} on v4") exec self.bin, cmd, *args end From d929b3e00072a96d99946aee88a3f8eea1a7a5b6 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 25 Sep 2015 10:32:53 -0700 Subject: [PATCH 724/952] show help for default commands --- lib/heroku/jsplugin.rb | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 9466e648a..0416b4ab5 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -4,7 +4,7 @@ class Heroku::JSPlugin extend Heroku::Helpers def self.try_takeover(command, args) - return if command == 'help' || args.include?('--help') + return if command == 'help' || args.include?('--help') || args.include?('-h') command = find_command(command) return if !command || command["hidden"] run(ARGV[0], nil, ARGV[1..-1]) @@ -39,6 +39,18 @@ def initialize(args, opts) :help => help, :hidden => plugin['hidden'], ) + if plugin['default'] + Heroku::Command.register_command( + :command => plugin['topic'], + :namespace => plugin['topic'], + :klass => klass, + :method => :run, + :banner => plugin['usage'], + :summary => " #{plugin['description']}", + :help => help, + :hidden => plugin['hidden'], + ) + end end end From d0456673a298fc1f387abb5cfd97f89a9378d304 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 11:47:52 -0400 Subject: [PATCH 725/952] remove run command --- lib/heroku/command/run.rb | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index 24d23d493..c4578673d 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -22,33 +22,6 @@ def self.push(cmd) # class Heroku::Command::Run < Heroku::Command::Base - # run COMMAND - # - # run an attached dyno - # - # -s, --size SIZE # specify dyno size - # --exit-code # return exit code from process - # - #Example: - # - # $ heroku run bash - # Running `bash` attached to terminal... up, run.1 - # ~ $ - # - # $ heroku run -s hobby -- myscript.sh -a arg1 -s arg2 - # Running `myscript.sh -a arg1 -s arg2` attached to terminal... up, run.1 - # - def index - if ARGV.include?('--') || ARGV.include?('--exit-code') - Heroku::JSPlugin.run('run', nil, ARGV[1..-1]) - return - end - command = args.join(" ") - error("Usage: heroku run COMMAND") if command.empty? - warn_if_using_jruby - run_attached(command) - end - # run:detached COMMAND # # run a detached dyno, where output is sent to your logs From e20522018af4c31ac4bfdd08a0836dbfc154584d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 12:01:26 -0400 Subject: [PATCH 726/952] v3.42.4 --- CHANGELOG | 9 +++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 90b48c022..9873bbe8c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +3.42.4 2015-09-28 +================= +Move v4 windows directory to LOCALAPPDIR +Move help logic to v3 +Fix cygwin setup +Fix for non-ascii windows home directories +Allow debian users to manually update v4 +Fully deprecate ddollar/heroku-accounts + 3.42.3 2015-09-22 ================= Require v4 CLI for all commands diff --git a/Gemfile.lock b/Gemfile.lock index 3bb1f29ef..8dc840998 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.3) + heroku (3.42.4) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1e4deefa5..2a427b4a7 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.3" + VERSION = "3.42.4" end From 5656034c2e011d437fbfe5e3835db94a279f9507 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 12:39:51 -0400 Subject: [PATCH 727/952] use LOCALAPPDATA on windows --- lib/heroku/jsplugin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 60515f2ba..cad86da16 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -103,8 +103,8 @@ def self.version end def self.app_dir - if os == 'windows' && ENV['LOCALAPPDIR'] - File.join(ENV['LOCALAPPDIR'], 'heroku') + if os == 'windows' && ENV['LOCALAPPDATA'] + File.join(ENV['LOCALAPPDATA'], 'heroku') else File.join(Heroku::Helpers.home_directory, '.heroku') end From 19dbbc06669b735242768f5c3f01e2e621b9e53d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 12:40:24 -0400 Subject: [PATCH 728/952] v3.42.5 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9873bbe8c..c7022d65f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.5 2015-09-28 +================= +Move v4 windows directory to LOCALAPPDATA + 3.42.4 2015-09-28 ================= Move v4 windows directory to LOCALAPPDIR diff --git a/Gemfile.lock b/Gemfile.lock index 8dc840998..f58e20e36 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.4) + heroku (3.42.5) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2a427b4a7..75dda630e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.4" + VERSION = "3.42.5" end From b3f168efb9937ea54393e779fc6019f0f0443d7b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 12:46:07 -0400 Subject: [PATCH 729/952] fix cygwin exec --- resources/exe/heroku | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/exe/heroku b/resources/exe/heroku index 5b7dc9c2d..3f24d8456 100755 --- a/resources/exe/heroku +++ b/resources/exe/heroku @@ -1,5 +1,5 @@ #!/bin/sh -exec "heroku.bat" +exec "heroku.bat" $@ #!/usr/bin/env ruby # encoding: UTF-8 From 90e693614067129a050ddcacb71f8d4052a6c78b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 12:46:35 -0400 Subject: [PATCH 730/952] v3.42.6 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c7022d65f..3e13a33ba 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.6 2015-09-28 +================= +Fix cygwin exec + 3.42.5 2015-09-28 ================= Move v4 windows directory to LOCALAPPDATA diff --git a/Gemfile.lock b/Gemfile.lock index f58e20e36..0b7ad310a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.5) + heroku (3.42.6) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 75dda630e..25f78ea06 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.5" + VERSION = "3.42.6" end From d12068f681f3a893b2b235fc2dff03a78689b9e1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:03:12 -0400 Subject: [PATCH 731/952] uppercase updating messages to match others --- lib/heroku/updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 78219e32c..91b0375b1 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -108,7 +108,7 @@ def self.warn_if_out_of_date def self.update(prerelease=false) return unless prerelease || needs_update? - stderr_print 'updating Heroku CLI...' + stderr_print 'Updating Heroku CLI...' wait_for_lock do require "tmpdir" require "zip" From 9231c77c2d32ebdbd6fc7be2ecae2b811a2b4e9c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:07:20 -0400 Subject: [PATCH 732/952] fix build --- spec/heroku/command/run_spec.rb | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/spec/heroku/command/run_spec.rb b/spec/heroku/command/run_spec.rb index ba47a7c75..71b942e0f 100644 --- a/spec/heroku/command/run_spec.rb +++ b/spec/heroku/command/run_spec.rb @@ -15,19 +15,6 @@ api.delete_app("example") end - describe "run" do - it "runs a command" do - stub_rendezvous.start { $stdout.puts "output" } - - stderr, stdout = execute("run bin/foo") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Running `bin/foo` attached to terminal... up, run.1 -output -STDOUT - end - end - describe "run:detached" do it "runs a command detached" do stderr, stdout = execute("run:detached bin/foo") From dd4afaed23b903b257dba834577e0fcb92307f94 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:32:45 -0400 Subject: [PATCH 733/952] correct location for heroku-cli --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index cad86da16..eb9feee92 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -111,7 +111,7 @@ def self.app_dir end def self.bin - File.join(app_dir, ".heroku", os == 'windows' ? 'heroku-cli.exe' : 'heroku-cli') + File.join(app_dir, os == 'windows' ? 'heroku-cli.exe' : 'heroku-cli') end def self.setup From c5c02addaf9e9fbece743889636e558ffa3132cf Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:33:34 -0400 Subject: [PATCH 734/952] v3.42.7 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3e13a33ba..29173d52a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.7 2015-09-29 +================= +Moved heroku-cli back to ~/.heroku/heroku-cli + 3.42.6 2015-09-28 ================= Fix cygwin exec diff --git a/Gemfile.lock b/Gemfile.lock index 0b7ad310a..ae33c6c00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.6) + heroku (3.42.7) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 25f78ea06..d4886f30a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.6" + VERSION = "3.42.7" end From ca71dba1beb183bb48d47068f95a651ddc0aaa70 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:43:34 -0400 Subject: [PATCH 735/952] fix login with --app commands --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index d27965278..7b959ee3f 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -274,7 +274,7 @@ def self.handle_auth_error(e) false else puts "Authentication failure" - run "login" + Heroku::JSPlugin.run('login', nil, []) true end end From cee4ccc454760850f81706716f960d9fac16b7b7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:44:22 -0400 Subject: [PATCH 736/952] v3.42.8 --- CHANGELOG | 6 +++++- lib/heroku/version.rb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 29173d52a..7f96f4977 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,8 @@ -3.42.7 2015-09-29 +3.42.8 2015-09-28 +================= +Fixed auto-login with --app flags + +3.42.7 2015-09-28 ================= Moved heroku-cli back to ~/.heroku/heroku-cli diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d4886f30a..9da4e3ed9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.7" + VERSION = "3.42.8" end From 1fa97e027979fabe7d0685f8ee845078e0cc172b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:48:00 -0400 Subject: [PATCH 737/952] fixed gemfile.lock --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ae33c6c00..e9e08e8e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.7) + heroku (3.42.8) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) From 36595850a82c9be0350c8caa59302ed5bfbf8700 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 13:49:45 -0400 Subject: [PATCH 738/952] v3.42.9 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7f96f4977..9ae0a0ce2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.9 2015-09-28 +================= +Updated Gemfile.lock + 3.42.8 2015-09-28 ================= Fixed auto-login with --app flags diff --git a/Gemfile.lock b/Gemfile.lock index e9e08e8e8..12cd06006 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.8) + heroku (3.42.9) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9da4e3ed9..14fa9af32 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.8" + VERSION = "3.42.9" end From 559b4c4f7efba7bfafeae8c2cba46f6095d1db04 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 18:20:21 -0400 Subject: [PATCH 739/952] removed addons index spec --- spec/heroku/command/addons_spec.rb | 75 ------------------------------ 1 file changed, 75 deletions(-) diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 4b3432bc3..216794e23 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -11,81 +11,6 @@ module Heroku::Command stub_core.release("example", "current").returns( "name" => "v99" ) end - describe "#index" do - before(:each) do - stub_core - api.post_app("name" => "example", "stack" => "cedar") - end - - after(:each) do - api.delete_app("example") - end - - it "should display no addons when none are configured" do - Excon.stub(method: :get, path: '/apps/example/addons') do - { body: "[]", status: 200 } - end - - Excon.stub(method: :get, path: '/apps/example/addon-attachments') do - { body: "[]", status: 200 } - end - - stderr, stdout = execute("addons") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== Resources for example -There are no add-ons. - -=== Attachments for example -There are no attachments. -STDOUT - - Excon.stubs.shift(2) - end - - it "should list addons and attachments" do - Excon.stub(method: :get, path: '/apps/example/addons') do - hooks = build_addon( - name: "swimming-nicely-42", - plan: { name: "deployhooks:http", price: { cents: 0, unit: "month" }}, - app: { name: "example" }) - - hpg = build_addon( - name: "jumping-slowly-76", - plan: { name: "heroku-postgresql:ronin", price: { cents: 20000, unit: "month" }}, - app: { name: "example" }) - - { body: MultiJson.encode([hooks, hpg]), status: 200 } - end - - Excon.stub(method: :get, path: '/apps/example/addon-attachments') do - hpg = build_attachment( - name: "HEROKU_POSTGRESQL_CYAN", - addon: { name: "heroku-postgresql-12345", app: { name: "example" }}, - app: { name: "example" }) - - { body: MultiJson.encode([hpg]), status: 200 } - end - - stderr, stdout = execute("addons") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== Resources for example -Plan Name Price ------------------------ ------------------ ------------- -deployhooks:http swimming-nicely-42 free -heroku-postgresql:ronin jumping-slowly-76 $200.00/month - -=== Attachments for example -Name Add-on Billing App ----------------------- ----------------------- ----------- -HEROKU_POSTGRESQL_CYAN heroku-postgresql-12345 example -STDOUT - Excon.stubs.shift(2) - end - - end - describe "list" do before do Excon.stub(method: :get, path: '/addon-services') do From 9eaf37da6ca9fe478c5f68f00c4178ca7e6f3b4b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 18:16:36 -0400 Subject: [PATCH 740/952] removed addons index (in v4 now) --- lib/heroku/command/addons.rb | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index c7d66070f..3af12d024 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -15,42 +15,6 @@ class Addons < Base include Heroku::Helpers::Addons::Display include Heroku::Helpers::Addons::Resolve - # addons [{--all,--app APP_NAME,--resource ADDON_NAME}] - # - # list installed add-ons - # - # NOTE: --all is the default unless in an application repository directory, in - # which case --all is inferred. - # - # --all # list add-ons across all apps in account - # --app APP_NAME # list add-ons associated with a given app - # --resource ADDON_NAME # view details about add-on and all of its attachments - # - #Examples: - # - # $ heroku addons --all - # $ heroku addons --app acme-inc-website - # $ heroku addons --resource @acme-inc-database - # - def index - validate_arguments! - requires_preauth - - # Filters are mutually exclusive - error("Can not use --all with --app") if options[:app] && options[:all] - error("Can not use --all with --resource") if options[:resource] && options[:all] - error("Can not use --app with --resource") if options[:resource] && options[:app] - - app = (self.app rescue nil) - if (resource = options[:resource]) - show_for_resource(resource) - elsif app && !options[:all] - show_for_app(app) - else - show_all - end - end - # addons:services # # list all available add-on services From aca7e574fb0458e7310874c106422b9abed78b8c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 18:17:04 -0400 Subject: [PATCH 741/952] copy change addon -> add-on --- lib/heroku/command/addons.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 3af12d024..9c600210b 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -6,7 +6,7 @@ module Heroku::Command - # manage addon resources + # manage add-on resources # class Addons < Base From abe481d9b1ccb7196bbc25f65505323c14c312f1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 18:26:35 -0400 Subject: [PATCH 742/952] v3.42.10 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9ae0a0ce2..7a32d7cec 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.10 2015-09-28 +================== +Moved addons to v4 + 3.42.9 2015-09-28 ================= Updated Gemfile.lock diff --git a/Gemfile.lock b/Gemfile.lock index 12cd06006..c6a691682 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.9) + heroku (3.42.10) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 14fa9af32..f08eea9a6 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.9" + VERSION = "3.42.10" end From 80397b8859be6f4dca1edcdb89fda9b86455d276 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 20:03:31 -0400 Subject: [PATCH 743/952] 3.42.11 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7a32d7cec..a71522b50 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.11 2015-09-28 +================== +Reverted addon removal until all v4 clients have it + 3.42.10 2015-09-28 ================== Moved addons to v4 diff --git a/Gemfile.lock b/Gemfile.lock index c6a691682..306a6d9c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.10) + heroku (3.42.11) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f08eea9a6..53bfd2e65 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.10" + VERSION = "3.42.11" end From e8df736af98a20dcf8b076293fee59786a627561 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 28 Sep 2015 20:04:13 -0400 Subject: [PATCH 744/952] Revert "removed addons index (in v4 now)" This reverts commit 9eaf37da6ca9fe478c5f68f00c4178ca7e6f3b4b. --- lib/heroku/command/addons.rb | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 9c600210b..8c73379e5 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -15,6 +15,42 @@ class Addons < Base include Heroku::Helpers::Addons::Display include Heroku::Helpers::Addons::Resolve + # addons [{--all,--app APP_NAME,--resource ADDON_NAME}] + # + # list installed add-ons + # + # NOTE: --all is the default unless in an application repository directory, in + # which case --all is inferred. + # + # --all # list add-ons across all apps in account + # --app APP_NAME # list add-ons associated with a given app + # --resource ADDON_NAME # view details about add-on and all of its attachments + # + #Examples: + # + # $ heroku addons --all + # $ heroku addons --app acme-inc-website + # $ heroku addons --resource @acme-inc-database + # + def index + validate_arguments! + requires_preauth + + # Filters are mutually exclusive + error("Can not use --all with --app") if options[:app] && options[:all] + error("Can not use --all with --resource") if options[:resource] && options[:all] + error("Can not use --app with --resource") if options[:resource] && options[:app] + + app = (self.app rescue nil) + if (resource = options[:resource]) + show_for_resource(resource) + elsif app && !options[:all] + show_for_app(app) + else + show_all + end + end + # addons:services # # list all available add-on services From 57a6abe10250ca9a419358338a1d6b34d8774ae1 Mon Sep 17 00:00:00 2001 From: Matthew Conway Date: Mon, 28 Sep 2015 22:58:49 -0700 Subject: [PATCH 745/952] Update some pg commands that require preauth --- lib/heroku/command/pg.rb | 1 + lib/heroku/command/pg_backups.rb | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index ee9b36e9e..8c8aafd79 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -513,6 +513,7 @@ def upgrade # destroy # Destroy a data link between a local and remote database # def links + requires_preauth mode = shift_argument || 'list' if !(%w(list create destroy).include?(mode)) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 778e99fe9..0ff3d21be 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -383,6 +383,7 @@ def capture_backup def restore_backup # heroku pg:backups restore [[backup_id] database] + requires_preauth db = nil restore_from = :latest @@ -394,6 +395,7 @@ def restore_backup db = shift_argument end + attachment = generate_resolver.resolve(db, "DATABASE_URL") validate_arguments! interval = options[:wait_interval].to_i || 3 From a810d64959f96921ac828c7d9e1246de4a7ad8de Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 29 Sep 2015 11:30:03 -0400 Subject: [PATCH 746/952] ensure help is rendered for v4 command even if they are in v3 --- lib/heroku/jsplugin.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index eb9feee92..949389932 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -4,7 +4,11 @@ class Heroku::JSPlugin extend Heroku::Helpers def self.try_takeover(command, args) - return if command == 'help' || args.include?('--help') || args.include?('-h') + if command == 'help' && args.length > 0 + return help(find_command(args[0])) + elsif args.include?('--help') || args.include?('-h') + return help(find_command(command)) + end command = find_command(command) return if !command || command["hidden"] run(ARGV[0], nil, ARGV[1..-1]) @@ -19,7 +23,7 @@ def self.load! ) unless topic['hidden'] || Heroku::Command.namespaces.include?(topic['name']) end commands.each do |plugin| - help = "\n\n #{plugin['fullHelp'].split("\n").join("\n ")}" + help = "\n\n #{plugin['fullHelp']}" klass = Class.new do def initialize(args, opts) @args = args @@ -198,4 +202,10 @@ def self.find_command(s) commands.find { |t| t["topic"] == topic && (t["command"] == nil || t["default"]) } end end + + def self.help(cmd) + return unless cmd + puts "Usage: heroku #{cmd['usage']}\n\n#{cmd['description']}\n\n#{cmd['fullHelp']}" + exit 0 + end end From d129779312876c13e7b5698e03192de81219346f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 29 Sep 2015 11:32:33 -0400 Subject: [PATCH 747/952] v3.42.12 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a71522b50..6f32318d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.12 2015-09-29 +================== +Fixed help for v4 commands that are overridden (such as `addons`) + 3.42.11 2015-09-28 ================== Reverted addon removal until all v4 clients have it diff --git a/Gemfile.lock b/Gemfile.lock index 306a6d9c9..5a07c9545 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.11) + heroku (3.42.12) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 53bfd2e65..4f7c6b6a9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.11" + VERSION = "3.42.12" end From dc69af36657bb85dda6ab293cc2720b553ea9a2a Mon Sep 17 00:00:00 2001 From: Matt Gauger Date: Wed, 30 Sep 2015 11:54:23 -0500 Subject: [PATCH 748/952] Indentation, whitespace --- spec/heroku/helpers/heroku_postgresql_spec.rb | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index ef4cc965c..3ba130724 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -40,21 +40,21 @@ } end - let(:app_attachments) { - [ Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_IVORY', - 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', - 'app' => {'name' => 'sushi' }, - 'resource' => {'name' => 'softly-mocking-123', - 'value' => 'postgres://default', - 'type' => 'heroku-postgresql:baku' }}), - Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_BLACK', - 'config_var' => 'HEROKU_POSTGRESQL_BLACK_URL', - 'app' => {'name' => 'sushi' }, - 'resource' => {'name' => 'quickly-yelling-2421', - 'value' => 'postgres://black', - 'type' => 'heroku-postgresql:zilla' }}) - ] - } + let(:app_attachments) { + [ Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', + 'app' => {'name' => 'sushi' }, + 'resource' => {'name' => 'softly-mocking-123', + 'value' => 'postgres://default', + 'type' => 'heroku-postgresql:baku' }}), + Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_BLACK', + 'config_var' => 'HEROKU_POSTGRESQL_BLACK_URL', + 'app' => {'name' => 'sushi' }, + 'resource' => {'name' => 'quickly-yelling-2421', + 'value' => 'postgres://black', + 'type' => 'heroku-postgresql:zilla' }}) + ] + } context "when the DATABASE_URL has query options" do let(:app_config_vars) do @@ -201,7 +201,5 @@ expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end - - end end From 879cd3fb192cd68b0c772d628b6d646f3125e7aa Mon Sep 17 00:00:00 2001 From: Matt Gauger Date: Wed, 30 Sep 2015 12:01:23 -0500 Subject: [PATCH 749/952] Trim whitespace --- spec/heroku/command/addons_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 4b3432bc3..72f06ce09 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -42,7 +42,7 @@ module Heroku::Command Excon.stubs.shift(2) end - + it "should list addons and attachments" do Excon.stub(method: :get, path: '/apps/example/addons') do hooks = build_addon( @@ -665,7 +665,7 @@ module Heroku::Command expect(@addons).to receive(:confirm_command).once.and_return(true) allow(@addons).to receive(:get_attachments).and_return([]) allow(@addons).to receive(:resolve_addon!).and_return({ - "id" => "abc123", + "id" => "abc123", "config_vars" => [], "app" => { "id" => "123", "name" => "example" } }) @@ -682,7 +682,7 @@ module Heroku::Command allow(@addons).to receive(:args).and_return(%w( addon1 )) allow(@addons).to receive(:get_attachments).and_return([]) allow(@addons).to receive(:resolve_addon!).and_return({ - "id" => "abc123", + "id" => "abc123", "config_vars" => [], "app" => { "id" => "123", "name" => "example" } }) @@ -746,7 +746,7 @@ module Heroku::Command ) } end - + expect { execute('addons:docs thing') }.to raise_error(Heroku::API::Errors::RequestFailed) end end From a471c9156d436c5c0988b737c0d638beca0be213 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 30 Sep 2015 13:49:42 -0400 Subject: [PATCH 750/952] v3.42.13 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6f32318d8..67312f219 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.13 2015-09-30 +================== +Resolve add-ons for pg:* commands with API + 3.42.12 2015-09-29 ================== Fixed help for v4 commands that are overridden (such as `addons`) diff --git a/Gemfile.lock b/Gemfile.lock index 5a07c9545..0247bf069 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.12) + heroku (3.42.13) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4f7c6b6a9..0db1deaae 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.12" + VERSION = "3.42.13" end From 67c94c07b4b088c8bf9202efcbe0663cbedc8590 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 30 Sep 2015 14:59:08 -0400 Subject: [PATCH 751/952] auto-remove non-updateable v4 clients --- lib/heroku/jsplugin.rb | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 949389932..325e7d9ac 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -107,7 +107,7 @@ def self.version end def self.app_dir - if os == 'windows' && ENV['LOCALAPPDATA'] + if windows? && ENV['LOCALAPPDATA'] File.join(ENV['LOCALAPPDATA'], 'heroku') else File.join(Heroku::Helpers.home_directory, '.heroku') @@ -115,11 +115,12 @@ def self.app_dir end def self.bin - File.join(app_dir, os == 'windows' ? 'heroku-cli.exe' : 'heroku-cli') + File.join(app_dir, windows? ? 'heroku-cli.exe' : 'heroku-cli') end def self.setup - return if File.exist? bin + check_if_old + return if setup? require 'excon' $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) @@ -137,6 +138,10 @@ def self.setup $stderr.puts " done.\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" end + def self.setup? + File.exist? bin + end + def self.copy_ca_cert to = File.join(app_dir, "cacert.pem") return if File.exists?(to) @@ -182,7 +187,7 @@ def self.manifest end def self.excon_opts - if os == 'windows' || ENV['HEROKU_SSL_VERIFY'] == 'disable' + if windows? || ENV['HEROKU_SSL_VERIFY'] == 'disable' # S3 SSL downloads do not work from ruby in Windows {:ssl_verify_peer => false} else @@ -208,4 +213,15 @@ def self.help(cmd) puts "Usage: heroku #{cmd['usage']}\n\n#{cmd['description']}\n\n#{cmd['fullHelp']}" exit 0 end + + # check if release is one that isn't able to update on windows + def self.check_if_old + File.delete(bin) if windows? && setup? && version.start_with?("heroku-cli/4.24") + rescue e + $stderr.puts e + end + + def self.windows? + os == 'windows' + end end From 981594244fc697269cf37f82bb13242613a5cf85 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 30 Sep 2015 15:37:01 -0400 Subject: [PATCH 752/952] reverted cygwin fix for windows bin it fixed cygwin, but it broke old versions of git bash --- resources/exe/heroku | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/exe/heroku b/resources/exe/heroku index 3f24d8456..7a7fd4ce5 100755 --- a/resources/exe/heroku +++ b/resources/exe/heroku @@ -1,5 +1,7 @@ #!/bin/sh -exec "heroku.bat" $@ +# find embedded ruby relative to script +bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` +exec "$bindir/ruby" -x "$0" "$@" #!/usr/bin/env ruby # encoding: UTF-8 From e654c7c74462fa56949fd3864f82062f7e809dec Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 30 Sep 2015 17:23:52 -0400 Subject: [PATCH 753/952] v3.42.14 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 67312f219..75e547913 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.42.14 2015-09-30 +================== +Ensure v4 is not 4.24.* which won't autoupdate on Windows +Revert cygwin patch in 3.42.6 since it broke git bash + 3.42.13 2015-09-30 ================== Resolve add-ons for pg:* commands with API diff --git a/Gemfile.lock b/Gemfile.lock index 0247bf069..1fbb8a28a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.13) + heroku (3.42.14) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0db1deaae..d31c9a516 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.13" + VERSION = "3.42.14" end From e81c5d954ba68033d57fcfe3b465cc1fb4b4681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Wed, 30 Sep 2015 15:28:53 -0700 Subject: [PATCH 754/952] Display message when available --- lib/heroku/command/pg.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 8c8aafd79..7fb4c4d9e 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -568,7 +568,11 @@ def links response = hpg_client(local_attachment).link_set(remote_attachment.name, options[:as]) - display("New link '#{response[:name]}' successfully created.") + if response.has_key?(:message) + output_with_bang(response[:message]) + else + display("New link '#{response[:name]}' successfully created.") + end when 'destroy' local = shift_argument link = shift_argument From 26727469cf18c61aa0ee2507599ba71224da1341 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 1 Oct 2015 00:22:07 -0400 Subject: [PATCH 755/952] ignore jsplugin error --- lib/heroku/jsplugin.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 325e7d9ac..c1e13a816 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -217,8 +217,7 @@ def self.help(cmd) # check if release is one that isn't able to update on windows def self.check_if_old File.delete(bin) if windows? && setup? && version.start_with?("heroku-cli/4.24") - rescue e - $stderr.puts e + rescue end def self.windows? From 11d4c7b1619d58ede21292c5e5b16dbd6d7110cb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 1 Oct 2015 00:23:04 -0400 Subject: [PATCH 756/952] v3.42.15 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 75e547913..0bc0a01ce 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.15 2015-09-31 +================== +Catch all errors when attempting to delete old v4 release + 3.42.14 2015-09-30 ================== Ensure v4 is not 4.24.* which won't autoupdate on Windows diff --git a/Gemfile.lock b/Gemfile.lock index 1fbb8a28a..7841a29c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.14) + heroku (3.42.15) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d31c9a516..68963f80a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.14" + VERSION = "3.42.15" end From 38f4351aaad45bbc23a0a5d57b0503c0079f60f5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 1 Oct 2015 00:30:36 -0400 Subject: [PATCH 757/952] report errors in removing old versions to rollbar --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index c1e13a816..fe439d9f3 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -217,6 +217,8 @@ def self.help(cmd) # check if release is one that isn't able to update on windows def self.check_if_old File.delete(bin) if windows? && setup? && version.start_with?("heroku-cli/4.24") + rescue => e + Rollbar.error(e) rescue end From f06599be04ef9bd0742b41a88d5e450d4660c106 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Mon, 5 Oct 2015 16:32:09 -0700 Subject: [PATCH 758/952] Improve handling of shareable add-ons w.r.t. Postgres backups Since Postgres backups are tied to the app and not the add-on, but we require *an* add-on to interact with, make sure that that add-on is attached to the right app. --- lib/heroku/command/pg_backups.rb | 4 +++- lib/heroku/helpers/heroku_postgresql.rb | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 0ff3d21be..e3dd95004 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -112,7 +112,9 @@ def resolve_db_or_url(name_or_url, default=nil) end def arbitrary_app_db - generate_resolver.all_databases.values.first + generate_resolver.all_databases.values.find do |attachment| + attachment.billing_app == app + end end def transfer_name(transfer) diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index d21f37d58..c2bec6aa8 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -7,7 +7,8 @@ module Heroku::Helpers::HerokuPostgresql extend Heroku::Helpers class Attachment - attr_reader :app, :name, :config_var, :resource_name, :url, :addon, :plan + attr_reader :app, :name, :config_var, :resource_name, + :url, :addon, :plan, :billing_app attr_reader :bastions, :bastion_key def initialize(raw) @@ -18,6 +19,7 @@ def initialize(raw) @resource_name = raw['resource']['name'] @url = raw['resource']['value'] @addon, @plan = raw['resource']['type'].split(':') + @billing_app = raw['resource']['billing_app']['name'] # Optional Bastion information for tunneling. if config = raw['config'] From 8d9957aa5baccee43d3a9802b63eae0feffcd7d4 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 6 Oct 2015 13:39:38 -0700 Subject: [PATCH 759/952] Fix specs --- spec/heroku/command/pg_backups_spec.rb | 15 ++++++++++----- spec/heroku/command/pg_spec.rb | 18 ++++++++++++------ spec/heroku/helpers/heroku_postgresql_spec.rb | 6 ++++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index a195af881..cf2a3b153 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -18,21 +18,24 @@ module Heroku::Command 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => ivory_url, - 'type' => 'heroku-postgresql:standard-0' }}), + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'example' } }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_GREEN', 'config_var' => 'HEROKU_POSTGRESQL_GREEN_URL', 'resource' => {'name' => 'softly-mocking-123', 'value' => green_url, - 'type' => 'heroku-postgresql:standard-0' }}), + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'example' } }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_RED', 'config_var' => 'HEROKU_POSTGRESQL_RED_URL', 'resource' => {'name' => 'whatever-something-2323', 'value' => red_url, - 'type' => 'heroku-postgresql:standard-0' }}) + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'example' } }}) ] end @@ -44,7 +47,8 @@ module Heroku::Command 'config_var' => 'HEROKU_POSTGRESQL_TEAL_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => teal_url, - 'type' => 'heroku-postgresql:standard-0' }}) + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'aux-example' } }}) ] end @@ -207,7 +211,8 @@ module Heroku::Command 'config_var' => 'ALSO_HEROKU_POSTGRESQL_IVORY_URL', 'resource' => {'name' => 'loudly-yelling-1239', 'value' => 'postgres:///not-actually-ivory', - 'type' => 'heroku-postgresql:standard-0' }}) + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'example' } }}) example_attachments << additional_attachment stub_pg.schedule({ hour: '07', timezone: 'UTC', schedule_name: 'HEROKU_POSTGRESQL_IVORY_URL' }) diff --git a/spec/heroku/command/pg_spec.rb b/spec/heroku/command/pg_spec.rb index 4a42ab242..e6b693571 100644 --- a/spec/heroku/command/pg_spec.rb +++ b/spec/heroku/command/pg_spec.rb @@ -12,21 +12,24 @@ def stub_attachments(extra_attachments=[]) 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => 'postgres://database_url', - 'type' => 'heroku-postgresql:ronin' }}), + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' } }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_RONIN', 'config_var' => 'HEROKU_POSTGRESQL_RONIN_URL', 'resource' => {'name' => 'softly-mocking-123', 'value' => 'postgres://ronin_database_url', - 'type' => 'heroku-postgresql:ronin' }}), + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' } }}), Heroku::Helpers::HerokuPostgresql::Attachment.new({ 'app' => {'name' => 'example'}, 'name' => 'HEROKU_POSTGRESQL_FOLLOW', 'config_var' => 'HEROKU_POSTGRESQL_FOLLOW_URL', 'resource' => {'name' => 'whatever-something-2323', 'value' => 'postgres://follow_database_url', - 'type' => 'heroku-postgresql:ronin' }}) + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' } }}) ].concat(extra_attachments)) end @@ -373,7 +376,8 @@ def stub_attachments(extra_attachments=[]) 'config_var' => remote + '_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => "postgres://someurl.test/#{remote}", - 'type' => 'heroku-postgresql:ronin'}}) + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' }}}) local_url = "postgres:///#{local}" dump_restore = double() @@ -413,7 +417,8 @@ def stub_attachments(extra_attachments=[]) 'config_var' => remote + '_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => "postgres://someurl.test/#{remote}", - 'type' => 'heroku-postgresql:ronin'}}) + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'example' }}}) local_url = "postgres:///#{local}" dump_restore = double() expect(pg).to receive(:resolve_heroku_attachment).and_return( @@ -467,7 +472,8 @@ def stub_attachments(extra_attachments=[]) 'config_var' => 'DATABASE_URL', 'resource' => {'name' => 'loudly-yelling-1232', 'value' => 'postgres://database_url', - 'type' => 'heroku-postgresql:ronin' }}) + 'type' => 'heroku-postgresql:ronin', + 'billing_app' => { 'name' => 'sushi' } }}) ]) stderr, stdout = execute("pg:links") diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index 3ba130724..f3cfd8fd2 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -46,13 +46,15 @@ 'app' => {'name' => 'sushi' }, 'resource' => {'name' => 'softly-mocking-123', 'value' => 'postgres://default', - 'type' => 'heroku-postgresql:baku' }}), + 'type' => 'heroku-postgresql:baku', + 'billing_app' => { 'name' => 'sushi' }}}), Attachment.new({ 'name' => 'HEROKU_POSTGRESQL_BLACK', 'config_var' => 'HEROKU_POSTGRESQL_BLACK_URL', 'app' => {'name' => 'sushi' }, 'resource' => {'name' => 'quickly-yelling-2421', 'value' => 'postgres://black', - 'type' => 'heroku-postgresql:zilla' }}) + 'type' => 'heroku-postgresql:zilla', + 'billing_app' => { 'name' => 'sushi' } }}) ] } From 066b2f2acade34510afbc616cecb22adf7b54208 Mon Sep 17 00:00:00 2001 From: Maciek Sakrejda Date: Tue, 6 Oct 2015 13:58:07 -0700 Subject: [PATCH 760/952] Add spec for listing backups for apps with other DBs attached --- Gemfile.lock | 3 --- spec/heroku/command/pg_backups_spec.rb | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7841a29c3..9d28737b2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,3 @@ DEPENDENCIES rr rspec webmock - -BUNDLED WITH - 1.10.6 diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index cf2a3b153..32c0d1a21 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -187,6 +187,24 @@ module Heroku::Command expect(stderr).to match(/example has no heroku-postgresql databases/) expect(stdout).to be_empty end + + it "ignores attached databases that belong to other billing apps" do + allow_any_instance_of(Heroku::Helpers::HerokuPostgresql::Resolver) + .to receive(:app_attachments) + .and_return([ Heroku::Helpers::HerokuPostgresql::Attachment + .new({ + 'app' => {'name' => 'example'}, + 'name' => 'HEROKU_POSTGRESQL_IVORY', + 'config_var' => 'HEROKU_POSTGRESQL_IVORY_URL', + 'resource' => {'name' => 'loudly-yelling-1232', + 'value' => ivory_url, + 'type' => 'heroku-postgresql:standard-0', + 'billing_app' => { 'name' => 'sushi' } }}) + ]) + stderr, stdout = execute("pg:backups schedules") + expect(stderr).to match(/example has no heroku-postgresql databases/) + expect(stdout).to be_empty + end end describe "heroku pg:backups schedule" do From 52f06733e3d0e3ebb444e33365b700f64e8c0669 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 1 Oct 2015 01:13:10 -0400 Subject: [PATCH 761/952] update asynchronously --- lib/heroku/updater.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 91b0375b1..2ceaee558 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -98,7 +98,12 @@ def self.autoupdate FileUtils.mkdir_p File.dirname(last_autoupdate_path) FileUtils.touch last_autoupdate_path return warn_if_out_of_date if disable - update + begin + fork { update } + rescue NotImplementedError + # cannot fork on windows + update + end end def self.warn_if_out_of_date From 9df1caa071aba5687c3019e1af2b1dc743161a86 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 7 Oct 2015 16:19:54 -0700 Subject: [PATCH 762/952] remove update notification since they happen asynchronously now --- lib/heroku/updater.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 2ceaee558..90cf47f8f 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -113,7 +113,6 @@ def self.warn_if_out_of_date def self.update(prerelease=false) return unless prerelease || needs_update? - stderr_print 'Updating Heroku CLI...' wait_for_lock do require "tmpdir" require "zip" @@ -144,7 +143,6 @@ def self.update(prerelease=false) FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path - stderr_puts "done. Updated to #{version}" version end end From 5aff0692254075c5fabe13c82c42e4cad9c8f6e7 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 7 Oct 2015 16:33:14 -0700 Subject: [PATCH 763/952] show update notification when not async --- lib/heroku/updater.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 90cf47f8f..b57f24fac 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -102,7 +102,9 @@ def self.autoupdate fork { update } rescue NotImplementedError # cannot fork on windows + stderr_print 'Updating Heroku CLI...' update + stderr_puts ' done.' end end From f9c6d184acd4a95e3db1a09bfeffb169d1415c33 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 7 Oct 2015 17:38:24 -0700 Subject: [PATCH 764/952] remove all files when uninstalling on windows --- resources/exe/heroku.iss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 4a1046819..aebe0824d 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -47,6 +47,10 @@ Filename: "{tmp}\rubyinstaller.exe"; Parameters: "/verysilent /noreboot /nocance Filename: "{tmp}\git.exe"; Parameters: "/silent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" +[UninstallDelete] +Type: filesandordirs; Name: "{localappdata}\heroku" +Type: filesandordirs; Name: "{%UserProfile}\.heroku" + [Code] function NeedsAddPath(Param: string): boolean; From 5ad3daa957406b72e047ea27670f9660c5422695 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 8 Oct 2015 10:37:34 -0700 Subject: [PATCH 765/952] get version after loading v4 --- lib/heroku/jsplugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index fe439d9f3..f9f6a6170 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -136,6 +136,7 @@ def self.setup raise 'SHA mismatch for heroku-cli' end $stderr.puts " done.\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" + version end def self.setup? From c1e7507b59c364c4bc53462d7bbe7d2ddea4c098 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 8 Oct 2015 10:38:07 -0700 Subject: [PATCH 766/952] do not delete v4 --- lib/heroku/jsplugin.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index f9f6a6170..4454b7710 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -80,8 +80,6 @@ def self.commands commands_info['commands'] rescue $stderr.puts "error loading plugin commands" - # Remove v4 if it is causing issues (for now) - File.delete(bin) rescue nil return [] end From 58672cfb519b85f9380f0cf96aa22410cac3ca6a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 8 Oct 2015 10:53:43 -0700 Subject: [PATCH 767/952] better raise of commands grabbing issues --- lib/heroku/jsplugin.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 4454b7710..bdcc51f01 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -71,20 +71,18 @@ def self.is_plugin_installed?(name) def self.topics commands_info['topics'] - rescue - $stderr.puts "error loading plugin topics" - return [] end def self.commands commands_info['commands'] - rescue - $stderr.puts "error loading plugin commands" - return [] end def self.commands_info - @commands_info ||= json_decode(`"#{bin}" commands --json`) + @commands_info ||= begin + info = json_decode(`"#{bin}" commands --json`) + error "error getting commands #{$?}" if $? != 0 + info + end end def self.install(name, opts={}) From 5443b8e2581556a7c92d0c2c5851e13caae7cb5f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 8 Oct 2015 10:56:06 -0700 Subject: [PATCH 768/952] v3.42.16 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 5 ++++- lib/heroku/version.rb | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0bc0a01ce..78845579f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.42.16 2015-10-08 +================== +Improve handling of shareable addons with postgres +Fix bug that deleted v4 if there was an issue reading commands +Ensure windows uninstall will remove everything +Update asynchronously +Report errors in removing old versions to rollbar + 3.42.15 2015-09-31 ================== Catch all errors when attempting to delete old v4 release diff --git a/Gemfile.lock b/Gemfile.lock index 9d28737b2..114b735c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.15) + heroku (3.42.16) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -100,3 +100,6 @@ DEPENDENCIES rr rspec webmock + +BUNDLED WITH + 1.10.6 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 68963f80a..68a176dd9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.15" + VERSION = "3.42.16" end From fa4f4d26979ceb40357b079a0122cd2435915ab5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 8 Oct 2015 11:09:34 -0700 Subject: [PATCH 769/952] fix encoding for command output --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index bdcc51f01..d5e9c0039 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -79,7 +79,7 @@ def self.commands def self.commands_info @commands_info ||= begin - info = json_decode(`"#{bin}" commands --json`) + info = json_decode(`"#{bin}" commands --json`.encode('utf-8')) error "error getting commands #{$?}" if $? != 0 info end From 97b4b21ffde703c9605a14c8d586aac84235282f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 8 Oct 2015 11:11:17 -0700 Subject: [PATCH 770/952] set encoding globally --- lib/heroku/cli.rb | 1 + lib/heroku/jsplugin.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index a5db37b52..01375aa8f 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -3,6 +3,7 @@ exit 1 end +Encoding.default_internal, Encoding.default_external = ['utf-8'] * 2 load('heroku/helpers.rb') # reload helpers after possible inject_loadpath load('heroku/updater.rb') # reload updater after possible inject_loadpath diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index d5e9c0039..bdcc51f01 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -79,7 +79,7 @@ def self.commands def self.commands_info @commands_info ||= begin - info = json_decode(`"#{bin}" commands --json`.encode('utf-8')) + info = json_decode(`"#{bin}" commands --json`) error "error getting commands #{$?}" if $? != 0 info end From 90a3263881ef8941289775c0a5af90bf62db9d74 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 9 Oct 2015 17:37:37 -0700 Subject: [PATCH 771/952] v3.42.17 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 78845579f..69e199fba 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.17 2015-10-09 +================== +Make utf-8 the default encoding + 3.42.16 2015-10-08 ================== Improve handling of shareable addons with postgres diff --git a/Gemfile.lock b/Gemfile.lock index 114b735c7..29020ee0a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.16) + heroku (3.42.17) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 68a176dd9..d7225aa3e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.16" + VERSION = "3.42.17" end From fa62c1d8fa9e4c4c937715e00ecaa697feba3a68 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Sat, 10 Oct 2015 10:53:24 -0700 Subject: [PATCH 772/952] increase timeout downloading v4 --- lib/heroku/jsplugin.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index bdcc51f01..f1e3b2a16 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -121,7 +121,10 @@ def self.setup $stderr.print "Installing Heroku Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) copy_ca_cert - opts = excon_opts.merge(:middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]) + opts = excon_opts.merge( + :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress], + :read_timeout => 300, + ) resp = Excon.get(url, opts) open(bin, "wb") do |file| file.write(resp.body) From 9bb8abd4d1aadbef4f8b082c5a7b1f9d3a98163f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Oct 2015 12:05:32 -0700 Subject: [PATCH 773/952] fix localappdata with non-ascii characters --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index f1e3b2a16..5f1c2c91f 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -104,7 +104,7 @@ def self.version def self.app_dir if windows? && ENV['LOCALAPPDATA'] - File.join(ENV['LOCALAPPDATA'], 'heroku') + File.join(ENV['LOCALAPPDATA'], 'heroku').encode('windows-1252') else File.join(Heroku::Helpers.home_directory, '.heroku') end From 8d805f04bf49e8294f138817f6e69d381f7ed75f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Oct 2015 12:07:53 -0700 Subject: [PATCH 774/952] v3.42.18 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 69e199fba..ff13ff876 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.42.17 2015-10-09 +================== +Fix LOCALAPPDATA dir on Windows with non-ASCII characters +Increase timeout when downloading v4 + 3.42.17 2015-10-09 ================== Make utf-8 the default encoding diff --git a/Gemfile.lock b/Gemfile.lock index 29020ee0a..f09bcc246 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.17) + heroku (3.42.18) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d7225aa3e..a77f1ad88 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.17" + VERSION = "3.42.18" end From cafe96ea880eaa446cfa83a8729fb863e36cd35b Mon Sep 17 00:00:00 2001 From: Will Leinweber Date: Tue, 13 Oct 2015 13:40:51 -0700 Subject: [PATCH 775/952] Remove 9.1 warning on pg:diagnose output --- lib/heroku/helpers/pg_diagnose.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/heroku/helpers/pg_diagnose.rb b/lib/heroku/helpers/pg_diagnose.rb index 7fd6822f0..bf6acc128 100644 --- a/lib/heroku/helpers/pg_diagnose.rb +++ b/lib/heroku/helpers/pg_diagnose.rb @@ -42,8 +42,6 @@ def generate_report(db_id) attachment = generate_resolver.resolve(db_id, "DATABASE_URL") validate_arguments! - warn_old_databases(attachment) - metrics = get_metrics(attachment) params = { @@ -60,13 +58,6 @@ def generate_report(db_id) :headers => {"Content-Type" => "application/json"}) end - def warn_old_databases(attachment) - @uri = URI.parse(attachment.url) # for #nine_two? - if !nine_two? - warn "WARNING: pg:diagnose is only fully supported on Postgres version >= 9.2. Some checks will be skipped.\n\n" - end - end - def get_metrics(attachment) unless attachment.starter_plan? hpg_client(attachment).metrics From 1a19a153aace9f9064bfcdd4bb5e968195b58180 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Oct 2015 14:14:46 -0700 Subject: [PATCH 776/952] list all commands for v4 help --- lib/heroku/command/help.rb | 2 +- lib/heroku/jsplugin.rb | 84 ++++++++++++++++++-------------------- 2 files changed, 40 insertions(+), 46 deletions(-) diff --git a/lib/heroku/command/help.rb b/lib/heroku/command/help.rb index d3488984d..4eba1336a 100644 --- a/lib/heroku/command/help.rb +++ b/lib/heroku/command/help.rb @@ -149,7 +149,7 @@ def help_for_command(name) display("Alias: #{name} redirects to #{command_alias}") name = command_alias end - if command = commands[name] + if command = Heroku::JSPlugin.find_command(name) || commands[name] puts "Usage: heroku #{command[:banner]}" if command[:help].strip.length > 0 diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 5f1c2c91f..76a75e215 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -5,9 +5,9 @@ class Heroku::JSPlugin def self.try_takeover(command, args) if command == 'help' && args.length > 0 - return help(find_command(args[0])) + return elsif args.include?('--help') || args.include?('-h') - return help(find_command(command)) + return end command = find_command(command) return if !command || command["hidden"] @@ -15,44 +15,24 @@ def self.try_takeover(command, args) end def self.load! - this = self topics.each do |topic| Heroku::Command.register_namespace( :name => topic['name'], :description => " #{topic['description']}" ) unless topic['hidden'] || Heroku::Command.namespaces.include?(topic['name']) end - commands.each do |plugin| - help = "\n\n #{plugin['fullHelp']}" - klass = Class.new do - def initialize(args, opts) - @args = args - @opts = opts - end - end - klass.send(:define_method, :run) do - this.run(plugin['topic'], plugin['command'], ARGV[1..-1]) - end - Heroku::Command.register_command( - :command => plugin['command'] ? "#{plugin['topic']}:#{plugin['command']}" : plugin['topic'], - :namespace => plugin['topic'], - :klass => klass, - :method => :run, - :banner => plugin['usage'], - :summary => " #{plugin['description']}", - :help => help, - :hidden => plugin['hidden'], - ) - if plugin['default'] + commands.each do |command| + Heroku::Command.register_command(command) + if command[:default] Heroku::Command.register_command( - :command => plugin['topic'], - :namespace => plugin['topic'], - :klass => klass, + :command => command[:namespace], + :namespace => command[:namespace], + :klass => command[:klass], :method => :run, - :banner => plugin['usage'], - :summary => " #{plugin['description']}", - :help => help, - :hidden => plugin['hidden'], + :banner => command[:banner], + :summary => command[:summary], + :help => command[:help], + :hidden => command[:hidden], ) end end @@ -74,7 +54,32 @@ def self.topics end def self.commands - commands_info['commands'] + @commands ||= begin + this = self + commands_info['commands'].map do |command| + help = "\n\n#{command['fullHelp']}" + klass = Class.new do + def initialize(args, opts) + @args = args + @opts = opts + end + end + klass.send(:define_method, :run) do + this.run(command['topic'], command['command'], ARGV[1..-1]) + end + { + :command => command['command'] ? "#{command['topic']}:#{command['command']}" : command['topic'], + :namespace => command['topic'], + :klass => klass, + :method => :run, + :banner => command['usage'], + :summary => " #{command['description']}", + :help => help, + :hidden => command['hidden'], + :default => command['default'], + } + end + end end def self.commands_info @@ -200,18 +205,7 @@ def self.url end def self.find_command(s) - topic, cmd = s.split(':', 2) - if cmd - commands.find { |t| t["topic"] == topic && t["command"] == cmd } - else - commands.find { |t| t["topic"] == topic && (t["command"] == nil || t["default"]) } - end - end - - def self.help(cmd) - return unless cmd - puts "Usage: heroku #{cmd['usage']}\n\n#{cmd['description']}\n\n#{cmd['fullHelp']}" - exit 0 + commands.find { |c| c[:command] == s } end # check if release is one that isn't able to update on windows From a986edf30e9d051e78250331e0608486d4f77fd0 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Oct 2015 14:27:20 -0700 Subject: [PATCH 777/952] v3.42.19 --- CHANGELOG | 6 +++++- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ff13ff876..15b3a7fd4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,8 @@ -3.42.17 2015-10-09 +3.42.19 2015-10-10 +================== +Fix help indices for v4 topics + +3.42.18 2015-10-09 ================== Fix LOCALAPPDATA dir on Windows with non-ASCII characters Increase timeout when downloading v4 diff --git a/Gemfile.lock b/Gemfile.lock index f09bcc246..93cb4d361 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.18) + heroku (3.42.19) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index a77f1ad88..a760d0172 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.18" + VERSION = "3.42.19" end From 9eb52c05ec71938bb50cf99ebede5625ff4f3dab Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Oct 2015 14:34:26 -0700 Subject: [PATCH 778/952] prevent running hidden commands --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 76a75e215..dc2744be6 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -10,7 +10,7 @@ def self.try_takeover(command, args) return end command = find_command(command) - return if !command || command["hidden"] + return if !command || command[:hidden] run(ARGV[0], nil, ARGV[1..-1]) end From c38f111ffa6c5139f19f341d13978d317c51a41f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 13 Oct 2015 14:35:25 -0700 Subject: [PATCH 779/952] v3.42.20 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 15b3a7fd4..629900ea2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.20 2015-10-10 +================== +Prevent running v4 hidden commands by default + 3.42.19 2015-10-10 ================== Fix help indices for v4 topics diff --git a/Gemfile.lock b/Gemfile.lock index 93cb4d361..e7f656172 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.19) + heroku (3.42.20) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index a760d0172..6bd7b730f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.19" + VERSION = "3.42.20" end From 68c7c60a66e5a408f8758cf3e33d9a8ce345e744 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Tue, 13 Oct 2015 21:12:34 -0700 Subject: [PATCH 780/952] Discontinue use of v3.domain-cname variant This removes the use of the merged v3.domain-cname API variant and replaces it with the mainline v3 API. --- ...{domains_v3_domain_cname.rb => domains_v3.rb} | 10 ++++------ lib/heroku/command/domains.rb | 2 +- spec/heroku/command/domains_spec.rb | 16 +++++++--------- 3 files changed, 12 insertions(+), 16 deletions(-) rename lib/heroku/api/{domains_v3_domain_cname.rb => domains_v3.rb} (69%) diff --git a/lib/heroku/api/domains_v3_domain_cname.rb b/lib/heroku/api/domains_v3.rb similarity index 69% rename from lib/heroku/api/domains_v3_domain_cname.rb rename to lib/heroku/api/domains_v3.rb index 1e0174a35..079f90a4f 100644 --- a/lib/heroku/api/domains_v3_domain_cname.rb +++ b/lib/heroku/api/domains_v3.rb @@ -1,15 +1,13 @@ module Heroku class API - # TODO: rename methods and filename after 3.domain-cname is merged - def get_domains_v3_domain_cname(app, range=nil) rsp = request( :expects => [200, 206], :method => :get, :path => "/apps/#{app}/domains", :headers => { - "Accept" => "application/vnd.heroku+json; version=3.domain-cname", - "Range" => range + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Range' => range } ) if rsp.headers['Next-Range'] @@ -25,8 +23,8 @@ def post_domains_v3_domain_cname(app, hostname) :method => :post, :path => "/apps/#{app}/domains", :headers => { - "Accept" => "application/vnd.heroku+json; version=3.domain-cname", - "Content-Type" => "application/json" + 'Accept' => 'application/vnd.heroku+json; version=3', + 'Content-Type' => 'application/json' }, body: Heroku::Helpers.json_encode({'hostname' => hostname}) ) diff --git a/lib/heroku/command/domains.rb b/lib/heroku/command/domains.rb index f2650cbc7..cf8d6f7f9 100644 --- a/lib/heroku/command/domains.rb +++ b/lib/heroku/command/domains.rb @@ -1,5 +1,5 @@ require "heroku/command/base" -require "heroku/api/domains_v3_domain_cname" +require "heroku/api/domains_v3" module Heroku::Command diff --git a/spec/heroku/command/domains_spec.rb b/spec/heroku/command/domains_spec.rb index 734ec0e71..9ec32b2ac 100644 --- a/spec/heroku/command/domains_spec.rb +++ b/spec/heroku/command/domains_spec.rb @@ -17,10 +17,9 @@ module Heroku::Command stub_core end - # TODO: rename after 3.domain-cname is merged - def stub_get_domains_v3_domain_cname(*custom_hostnames) + def stub_get_domains_v3(*custom_hostnames) Excon.stub( - :headers => { "Accept" => "application/vnd.heroku+json; version=3.domain-cname" }, + :headers => { "Accept" => "application/vnd.heroku+json; version=3" }, :method => :get, :path => '/apps/example/domains') do { @@ -42,11 +41,10 @@ def stub_get_domains_v3_domain_cname(*custom_hostnames) end end - # TODO: rename after 3.domain-cname is merged - def stub_post_domains_v3_domain_cname(custom_hostname) + def stub_post_domains_v3(custom_hostname) Excon.stub( :headers => { - "Accept" => "application/vnd.heroku+json; version=3.domain-cname", + "Accept" => "application/vnd.heroku+json; version=3", "Content-Type" => "application/json" }, :method => :post, @@ -80,7 +78,7 @@ def stub_post_domains_v3_domain_cname(custom_hostname) end it "lists message with development domain but no custom domains" do - stub_get_domains_v3_domain_cname() + stub_get_domains_v3() stderr, stdout = execute("domains") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT @@ -94,7 +92,7 @@ def stub_post_domains_v3_domain_cname(custom_hostname) end it "lists development and custom domains when some exist" do - stub_get_domains_v3_domain_cname('example1.com', 'example2.com') + stub_get_domains_v3('example1.com', 'example2.com') stderr, stdout = execute("domains") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT @@ -112,7 +110,7 @@ def stub_post_domains_v3_domain_cname(custom_hostname) end it "adds domain names" do - stub_post_domains_v3_domain_cname('example.com') + stub_post_domains_v3('example.com') stderr, stdout = execute("domains:add example.com") expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT From 87189a5ced2dc25a17c451afd218a0c1332b38bf Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Oct 2015 10:08:22 -0700 Subject: [PATCH 781/952] increase update time to match v4 --- lib/heroku/updater.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index b57f24fac..632d99ddd 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -91,9 +91,9 @@ def self.wait_for_lock(wait_for=5, check_every=0.5) end def self.autoupdate - # if we've updated in the last hour, don't try again + # if we've updated in the last 4 hours, don't try again if File.exists?(last_autoupdate_path) - return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60 + return if (Time.now.to_i - File.mtime(last_autoupdate_path).to_i) < 60*60*4 end FileUtils.mkdir_p File.dirname(last_autoupdate_path) FileUtils.touch last_autoupdate_path From 0b3b75e6374b47b90a616ab3dd9c044313bc7cad Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Oct 2015 10:09:48 -0700 Subject: [PATCH 782/952] only show update message when not async --- lib/heroku/command/update.rb | 2 +- lib/heroku/updater.rb | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index e89bfa27f..eaf7e74b6 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -36,6 +36,6 @@ def beta def update_from_url(prerelease) Heroku::Updater.check_disabled! - Heroku::Updater.update(prerelease) + Heroku::Updater.update(prerelease, true) end end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 632d99ddd..edfd43233 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -99,11 +99,10 @@ def self.autoupdate FileUtils.touch last_autoupdate_path return warn_if_out_of_date if disable begin - fork { update } + fork { update(false, false) } rescue NotImplementedError # cannot fork on windows - stderr_print 'Updating Heroku CLI...' - update + update(false, true) stderr_puts ' done.' end end @@ -112,9 +111,10 @@ def self.warn_if_out_of_date $stderr.puts "WARNING: Toolbelt v#{latest_version} update available." if needs_minor_update? end - def self.update(prerelease=false) + def self.update(prerelease=false, message=true) return unless prerelease || needs_update? + stderr_puts 'Updating Heroku CLI...' wait_for_lock do require "tmpdir" require "zip" @@ -145,6 +145,8 @@ def self.update(prerelease=false) FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path + stderr_puts ' done.' + version end end From b54b72a00f18025054ac64a16c972a30fe75653b Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Oct 2015 10:48:41 -0700 Subject: [PATCH 783/952] added osx cert --- resources/pkg/certificate.p12.gpg | Bin 0 -> 3250 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/pkg/certificate.p12.gpg diff --git a/resources/pkg/certificate.p12.gpg b/resources/pkg/certificate.p12.gpg new file mode 100644 index 0000000000000000000000000000000000000000..baa78349eef3cfa4b5f9674f5880d0b14433739a GIT binary patch literal 3250 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 Date: Fri, 16 Oct 2015 13:32:02 -0700 Subject: [PATCH 784/952] use v4 for login --- lib/heroku/auth.rb | 5 +++-- lib/heroku/command.rb | 2 +- lib/heroku/jsplugin.rb | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index d98ad10cc..330e00372 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -42,7 +42,8 @@ def client end def login - delete_credentials + Heroku::JSPlugin.spawn('login', '', []) or exit + @netrc, @api, @client, @credentials = nil get_credentials end @@ -108,7 +109,7 @@ def api_key(user=get_credentials[0], password=get_credentials[1]) end def get_credentials # :nodoc: - @credentials ||= (read_credentials || ask_for_and_save_credentials) + @credentials ||= (read_credentials || login) end def delete_credentials diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 7b959ee3f..503823d23 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -274,7 +274,7 @@ def self.handle_auth_error(e) false else puts "Authentication failure" - Heroku::JSPlugin.run('login', nil, []) + Heroku::Auth.login true end end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index dc2744be6..e4e2e9486 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -159,6 +159,11 @@ def self.run(topic, command, args) exec self.bin, cmd, *args end + def self.spawn(topic, command, args) + cmd = command ? "#{topic}:#{command}" : topic + system self.bin, cmd, *args + end + def self.arch case RbConfig::CONFIG['host_cpu'] when /x86_64/ From f7e5e47793f0e5f3a697472238d0159d55736e60 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 16 Oct 2015 13:36:46 -0700 Subject: [PATCH 785/952] removed outdated spec --- spec/heroku/auth_spec.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index 01d1f2277..2098a67fb 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -121,13 +121,6 @@ module Heroku end end - it "asks for credentials when the file doesn't exist" do - @cli.delete_credentials - expect(@cli).to receive(:ask_for_credentials).and_return(["u", "p"]) - expect(@cli.user).to eq('u') - expect(@cli.password).to eq('p') - end - it "writes credentials and uploads authkey when credentials are saved" do allow(@cli).to receive(:credentials) allow(@cli).to receive(:check) From 22108b27f9e26f9b797139b13749125e5a236902 Mon Sep 17 00:00:00 2001 From: John Beynon Date: Mon, 19 Oct 2015 12:16:58 +0100 Subject: [PATCH 786/952] Fix documentation for window --- lib/heroku/command/pg.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 7fb4c4d9e..0b92cbbce 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -413,7 +413,7 @@ def pull end - # pg:maintenance + # pg:maintenance # # manage maintenance for # info # show current maintenance information From 14d4df01b1eee64a190167f14ab1ce5aa2fee0ff Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Wed, 21 Oct 2015 12:05:21 -0500 Subject: [PATCH 787/952] Remove protocol prefix for buildpack URNs when displaying and updating --- lib/heroku/command/buildpacks.rb | 7 ++++- spec/heroku/command/buildpacks_spec.rb | 36 ++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 1ec0ea43a..2ac382a82 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -186,11 +186,12 @@ def get_index(default=nil) end def update_buildpacks(buildpack_urls, action) - api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => url} }}) + api.put_app_buildpacks_v3(app, {:updates => buildpack_urls.map{|url| {:buildpack => to_buildpack_name(url)} }}) display_buildpack_change(buildpack_urls, action) end def display_buildpacks(buildpacks, indent=" ") + buildpacks.map!{|bp| to_buildpack_name(bp)} if (buildpacks.size == 1) display(buildpacks.first) else @@ -226,5 +227,9 @@ def display_no_buildpacks(action="removed", plural=false) end end + def to_buildpack_name(buildpack_url) + buildpack_url.gsub(/^urn:buildpack:/, '') + end + end end diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index cfccbd491..648dba0a1 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -53,6 +53,23 @@ def stub_get(*buildpacks) STDOUT end + context "with buildpack URNs" do + before(:each) do + Excon.stubs.shift + stub_get("urn:buildpack:heroku/nodejs", "urn:buildpack:heroku/ruby") + end + + it "displays the short-hand name" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URLs +1. heroku/nodejs +2. heroku/ruby + STDOUT + end + end + context "with no buildpack URL set" do before(:each) do Excon.stubs.shift @@ -230,6 +247,25 @@ def stub_get(*buildpacks) end describe "add" do + context "with buildpack URNs" do + before(:each) do + Excon.stubs.shift + stub_get("urn:buildpack:heroku/nodejs") + stub_put("heroku/nodejs", "heroku/ruby") + end + + it "displays the short-hand name" do + stderr, stdout = execute("buildpacks:add heroku/ruby") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +Buildpack added. Next release on example will use: + 1. heroku/nodejs + 2. heroku/ruby +Run `git push heroku master` to create a new release using these buildpacks. + STDOUT + end + end + context "with no buildpacks" do before(:each) do Excon.stubs.shift From 921bc0768cd013d55ed8d296af041ae93f5f4fbb Mon Sep 17 00:00:00 2001 From: Joe Kutner Date: Thu, 29 Oct 2015 18:48:35 -0500 Subject: [PATCH 788/952] Added gsub for official buildpack S3 URL --- lib/heroku/command/buildpacks.rb | 4 +++- spec/heroku/command/buildpacks_spec.rb | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/buildpacks.rb b/lib/heroku/command/buildpacks.rb index 2ac382a82..35d77ece5 100644 --- a/lib/heroku/command/buildpacks.rb +++ b/lib/heroku/command/buildpacks.rb @@ -228,7 +228,9 @@ def display_no_buildpacks(action="removed", plural=false) end def to_buildpack_name(buildpack_url) - buildpack_url.gsub(/^urn:buildpack:/, '') + buildpack_url. + gsub(/^urn:buildpack:/, ''). + gsub(%r{^https://codon-buildpacks\.s3\.amazonaws\.com/buildpacks/heroku/(.*)\.tgz$}, 'heroku/\1') end end diff --git a/spec/heroku/command/buildpacks_spec.rb b/spec/heroku/command/buildpacks_spec.rb index 648dba0a1..2fb532358 100644 --- a/spec/heroku/command/buildpacks_spec.rb +++ b/spec/heroku/command/buildpacks_spec.rb @@ -70,6 +70,23 @@ def stub_get(*buildpacks) end end + context "with offical buildpack URLs" do + before(:each) do + Excon.stubs.shift + stub_get("https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/nodejs.tgz", "https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/ruby.tgz") + end + + it "displays the short-hand name" do + stderr, stdout = execute("buildpacks") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT +=== example Buildpack URLs +1. heroku/nodejs +2. heroku/ruby + STDOUT + end + end + context "with no buildpack URL set" do before(:each) do Excon.stubs.shift From 9edd5d1e1c14a3b8e12993ec3c5dd7926eb7bb1a Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 29 Oct 2015 17:06:28 -0700 Subject: [PATCH 789/952] hide updating message when forked --- lib/heroku/updater.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index edfd43233..294985fc8 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -103,7 +103,6 @@ def self.autoupdate rescue NotImplementedError # cannot fork on windows update(false, true) - stderr_puts ' done.' end end @@ -114,7 +113,7 @@ def self.warn_if_out_of_date def self.update(prerelease=false, message=true) return unless prerelease || needs_update? - stderr_puts 'Updating Heroku CLI...' + stderr_puts 'Updating Heroku CLI...' if message wait_for_lock do require "tmpdir" require "zip" @@ -145,7 +144,7 @@ def self.update(prerelease=false, message=true) FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path - stderr_puts ' done.' + stderr_puts ' done.' if message version end From 31c3716b7ffb7051f23d06e2096ee56905ec3862 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Fri, 30 Oct 2015 08:42:20 -0700 Subject: [PATCH 790/952] v3.42.21 --- CHANGELOG | 8 ++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 629900ea2..bf6ed520f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +3.42.21 2015-10-30 +================== +Remove protocol prefix for buildpack URN +Fix documentation for pg:maintenance +Use v4 for login +Hide updating on windows +Remove v3 domain name variant + 3.42.20 2015-10-10 ================== Prevent running v4 hidden commands by default diff --git a/Gemfile.lock b/Gemfile.lock index e7f656172..3db883ad6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.20) + heroku (3.42.21) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 6bd7b730f..cceb51527 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.20" + VERSION = "3.42.21" end From ea806435da60d45ee6914dc84b451e5a3cc6bd82 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 2 Nov 2015 17:35:22 -0800 Subject: [PATCH 791/952] prefix update messaging add heroku-cli: so it is clear where the stderr messages are coming from --- lib/heroku/jsplugin.rb | 2 +- lib/heroku/updater.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index e4e2e9486..f1e95c13a 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -123,7 +123,7 @@ def self.setup check_if_old return if setup? require 'excon' - $stderr.print "Installing Heroku Toolbelt v4..." + $stderr.print "heroku-cli: Installing Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) copy_ca_cert opts = excon_opts.merge( diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 294985fc8..3c039876a 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -113,7 +113,7 @@ def self.warn_if_out_of_date def self.update(prerelease=false, message=true) return unless prerelease || needs_update? - stderr_puts 'Updating Heroku CLI...' if message + $stderr.print 'heroku-cli: Updating...' if message wait_for_lock do require "tmpdir" require "zip" @@ -144,7 +144,7 @@ def self.update(prerelease=false, message=true) FileUtils.mkdir_p File.dirname(updated_client_path) FileUtils.cp_r download_dir, updated_client_path - stderr_puts ' done.' if message + $stderr.puts ' done.' if message version end From 8d883d275a7dc7b82c890255d8f24ec3534a5f42 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 3 Nov 2015 16:59:15 -0800 Subject: [PATCH 792/952] update v4 plugins when running plugins:update --- lib/heroku/command/plugins.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 61018b086..ec7f84abb 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -84,6 +84,7 @@ def uninstall # Updating heroku-production-check... done # def update + Heroku::JSPlugin.update plugins = if plugin = shift_argument [plugin] else From 4f9e18d2247c9ec3fcc38477ea59cd638fa20e28 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 5 Nov 2015 11:10:03 -0600 Subject: [PATCH 793/952] Adding verification test for config:get not found --- spec/heroku/command/config_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/heroku/command/config_spec.rb b/spec/heroku/command/config_spec.rb index ee4dfa45a..888cd5b68 100644 --- a/spec/heroku/command/config_spec.rb +++ b/spec/heroku/command/config_spec.rb @@ -77,6 +77,15 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +STDOUT + end + + it "shows a single config for get not found" do + api.put_config_vars("example", { 'LONG' => 'A' * 60 }) + stderr, stdout = execute("config:get BLAH") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT + STDOUT end From 49e40e4cd2ad88ecd4b5a4f03d3e2cfba86af054 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 5 Nov 2015 10:10:38 -0800 Subject: [PATCH 794/952] use $XDG_DATA_HOME if available --- lib/heroku/jsplugin.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index f1e95c13a..527747ee4 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -110,6 +110,8 @@ def self.version def self.app_dir if windows? && ENV['LOCALAPPDATA'] File.join(ENV['LOCALAPPDATA'], 'heroku').encode('windows-1252') + elsif ENV['XDG_DATA_HOME'] + File.join(ENV['XDG_DATA_HOME'], 'heroku') else File.join(Heroku::Helpers.home_directory, '.heroku') end From 213daec0d004983ebc1fcfbcae287d0eebc36200 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 5 Nov 2015 12:16:13 -0600 Subject: [PATCH 795/952] Adding test for config:get --shell not found --- spec/heroku/command/config_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/heroku/command/config_spec.rb b/spec/heroku/command/config_spec.rb index 888cd5b68..4414f964e 100644 --- a/spec/heroku/command/config_spec.rb +++ b/spec/heroku/command/config_spec.rb @@ -86,6 +86,15 @@ module Heroku::Command expect(stderr).to eq("") expect(stdout).to eq <<-STDOUT +STDOUT + end + + it "shows a single config for get --shell when missing" do + api.put_config_vars("example", { 'LONG' => 'A' * 60 }) + stderr, stdout = execute("config:get --shell BLAH") + expect(stderr).to eq("") + expect(stdout).to eq <<-STDOUT + STDOUT end From 56a6d9e931b1fbc5baf99ff1ee7c2240ccc57f39 Mon Sep 17 00:00:00 2001 From: Bo Jeanes Date: Fri, 13 Nov 2015 15:03:38 +1100 Subject: [PATCH 796/952] Use attachment SSO url if it can be determined This is a bit finicky compared to the Dashboard because we're getting an arbitrary identifier which might be for an add-on or an attachments. This attempts to resolve an attachment with the identifier but silently falls back on add-on if it gives either a 404 or a 422 for multiple matches. --- lib/heroku/command/addons.rb | 15 ++++++++-- spec/heroku/command/addons_spec.rb | 45 ++++++++++++++++++++++++++++++ spec/support/addons_helper.rb | 3 +- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/addons.rb b/lib/heroku/command/addons.rb index 8c73379e5..326723881 100644 --- a/lib/heroku/command/addons.rb +++ b/lib/heroku/command/addons.rb @@ -375,10 +375,21 @@ def open requires_preauth addon = resolve_addon!(addon_name) - return addon if addon.is_a?(String) + web_url = addon['web_url'] + + begin + attachment = resolve_attachment!(addon_name) + web_url = attachment['web_url'] + rescue Heroku::API::Errors::NotFound + # no-op + rescue Heroku::API::Errors::RequestFailed => e + if MultiJson.decode(e.response.body)["id"] != "multiple_matches" + raise + end + end service = addon['addon_service']['name'] - launchy("Opening #{service} (#{addon['name']}) for #{addon['app']['name']}", addon["web_url"]) + launchy("Opening #{service} (#{addon['name']}) for #{addon['app']['name']}", web_url) end private diff --git a/spec/heroku/command/addons_spec.rb b/spec/heroku/command/addons_spec.rb index 72f06ce09..16b8cea01 100644 --- a/spec/heroku/command/addons_spec.rb +++ b/spec/heroku/command/addons_spec.rb @@ -771,7 +771,52 @@ module Heroku::Command end it "opens the addon if only one matches" do + Excon.stub(method: :get, path: %r'(/apps/example)?/addon-attachments/redistogo:nano') do + {status: 404} + end addon.merge!(addon_service: { name: "redistogo:nano" }) + + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon!).and_return(stringify(addon)) + require("launchy") + expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/#{addon[:id]}").and_return(Thread.new {}) + stderr, stdout = execute('addons:open redistogo:nano') + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT +Opening redistogo:nano (my_addon) for example... done +STDOUT + end + + it "opens the addon using the attachment URL if a single unique attachment can be determined" do + addon.merge!(addon_service: { name: "redistogo:nano" }) + attachment = build_attachment( + name: "REDISTOGO", + addon: { name: "redistogo-angular", app: { name: "example" }}, + app: { name: "example" }) + + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon!).and_return(stringify(addon)) + allow_any_instance_of(Heroku::Command::Addons).to receive(:get_attachment).and_return(stringify(attachment)) + require("launchy") + expect(Launchy).to receive(:open).with("https://attachment-sso").and_return(Thread.new {}) + stderr, stdout = execute('addons:open redistogo:nano') + expect(stderr).to eq('') + expect(stdout).to eq <<-STDOUT +Opening redistogo:nano (my_addon) for example... done +STDOUT + end + + it "opens the add-on using the add-on URL if a single unique attachment can not be determined" do + Excon.stub(method: :get, path: '/apps/example/addon-attachments/redistogo:nano') do + { + status: 422, + body: MultiJson.encode( + id: "multiple_matches", + message: "Ambiguous identifier; ..." + ) + } + end + + addon.merge!(addon_service: { name: "redistogo:nano" }) + allow_any_instance_of(Heroku::Command::Addons).to receive(:resolve_addon!).and_return(stringify(addon)) require("launchy") expect(Launchy).to receive(:open).with("https://addons-sso.heroku.com/apps/example/addons/#{addon[:id]}").and_return(Thread.new {}) diff --git a/spec/support/addons_helper.rb b/spec/support/addons_helper.rb index 949ae8309..0bb9ca108 100644 --- a/spec/support/addons_helper.rb +++ b/spec/support/addons_helper.rb @@ -42,7 +42,8 @@ def build_attachment(attachment={}) created_at: Time.now, id: attachment.fetch(:id, SecureRandom.uuid), name: attachment[:name], - updated_at: Time.now + updated_at: Time.now, + web_url: "https://attachment-sso" } end From 98db024a843e3686df36883195924938d35c44ce Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 12 Nov 2015 13:56:23 -0800 Subject: [PATCH 797/952] Fixing tests so they can run under windows --- spec/heroku/command/keys_spec.rb | 3 +- spec/heroku/plugin_spec.rb | 51 ++++++++++++++++++-------------- spec/heroku/updater_spec.rb | 4 +-- spec/spec_helper.rb | 8 ++++- 4 files changed, 39 insertions(+), 27 deletions(-) diff --git a/spec/heroku/command/keys_spec.rb b/spec/heroku/command/keys_spec.rb index 108d5b35e..d6f3632d7 100644 --- a/spec/heroku/command/keys_spec.rb +++ b/spec/heroku/command/keys_spec.rb @@ -20,7 +20,8 @@ module Heroku::Command Would you like to generate one? [Yn] Generating new SSH public key. Uploading SSH public key #{Heroku::Auth.home_directory}/.ssh/id_rsa.pub... done STDOUT - api.delete_key(`whoami`.strip + '@' + `hostname`.strip) + id_rsa_pub = File.read("#{Heroku::Auth.home_directory}/.ssh/id_rsa.pub") + api.delete_key(id_rsa_pub.split(' ')[2]) end it "adds a key from a specified keyfile path" do diff --git a/spec/heroku/plugin_spec.rb b/spec/heroku/plugin_spec.rb index 599161fe1..09b1ca89d 100644 --- a/spec/heroku/plugin_spec.rb +++ b/spec/heroku/plugin_spec.rb @@ -1,5 +1,6 @@ require "spec_helper" require "heroku/plugin" +require "tmpdir" module Heroku describe Plugin do @@ -7,7 +8,7 @@ module Heroku it "lives in ~/.heroku/plugins" do allow(Plugin).to receive(:home_directory).and_return('/home/user') - expect(Plugin.directory).to eq('/home/user/.heroku/plugins') + expect(Plugin.directory).to eq(File.expand_path('/home/user/.heroku/plugins')) end it "extracts the name from git urls" do @@ -16,14 +17,16 @@ module Heroku describe "management" do before(:each) do - @sandbox = "/tmp/heroku_plugins_spec_#{Process.pid}" - FileUtils.mkdir_p(@sandbox) + @sandbox = Dir.mktmpdir + @plugin_folder = File.join(Dir.mktmpdir, 'heroku_plugin') + FileUtils.mkdir_p(@plugin_folder) allow(Dir).to receive(:pwd).and_return(@sandbox) allow(Plugin).to receive(:directory).and_return(@sandbox) end after(:each) do FileUtils.rm_rf(@sandbox) + FileUtils.rm_rf(@plugin_folder) end it "lists installed plugins" do @@ -34,43 +37,45 @@ module Heroku end it "installs pulling from the plugin url" do - plugin_folder = "/tmp/heroku_plugin" - FileUtils.mkdir_p(plugin_folder) - `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` - Plugin.new(plugin_folder).install + `cd #{@plugin_folder} && git init && echo test > README && git add README && git commit -m my_plugin` + Plugin.new(@plugin_folder).install expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy - expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("test\n") + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq(File.read("#{@plugin_folder}/README")) end it "reinstalls over old copies" do - plugin_folder = "/tmp/heroku_plugin" - FileUtils.mkdir_p(plugin_folder) - `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` - Plugin.new(plugin_folder).install - Plugin.new(plugin_folder).install + `cd #{@plugin_folder} && git init && echo test > README && git add . && git commit -m my_plugin` + Plugin.new(@plugin_folder).install + Plugin.new(@plugin_folder).install expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy - expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("test\n") + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq(File.read("#{@plugin_folder}/README")) end context "update" do before(:each) do - plugin_folder = "/tmp/heroku_plugin" - FileUtils.mkdir_p(plugin_folder) - `cd #{plugin_folder} && git init && echo 'test' > README && git add . && git commit -m 'my plugin'` - Plugin.new(plugin_folder).install - `cd #{plugin_folder} && echo 'updated' > README && git add . && git commit -m 'my plugin update'` + @plugin_folder = File.join(Dir.mktmpdir, 'heroku_plugin') + FileUtils.mkdir_p(@plugin_folder) + `cd #{@plugin_folder} && git init && echo test > README && git add . && git commit -m my_plugin` + Plugin.new(@plugin_folder).install + `cd #{@plugin_folder} && echo updated > README && git add . && git commit -m my_plugin_update` end it "updates existing copies" do Plugin.new('heroku_plugin').update expect(File.directory?("#{@sandbox}/heroku_plugin")).to be_truthy - expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq("updated\n") + expect(File.read("#{@sandbox}/heroku_plugin/README")).to eq(File.read("#{@plugin_folder}/README")) end - it "raises exception on symlinked plugins" do - `cd #{@sandbox} && ln -s heroku_plugin heroku_plugin_symlink` - expect { Plugin.new('heroku_plugin_symlink').update }.to raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin + if Heroku::Helpers.running_on_windows? + xit "raises exception on symlinked plugins" do + # using mklink is problematic & buggy + end + else + it "raises exception on symlinked plugins" do + `cd #{@sandbox} && ln -s heroku_plugin heroku_plugin_symlink` + expect { Plugin.new('heroku_plugin_symlink').update }.to raise_error Heroku::Plugin::ErrorUpdatingSymlinkPlugin + end end end diff --git a/spec/heroku/updater_spec.rb b/spec/heroku/updater_spec.rb index cd9ecec51..16a4002ff 100644 --- a/spec/heroku/updater_spec.rb +++ b/spec/heroku/updater_spec.rb @@ -56,7 +56,7 @@ module Heroku describe 'non-beta' do before do - zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) + zip = IO.binread(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) hash = "615792e1f06800a6d744f518887b10c09aa914eab51d0f7fbbefd81a8a64af93" Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/zip'}, {:body => zip}) Excon.stub({:host => 'toolbelt.heroku.com', :path => '/update/hash'}, {:body => "#{hash}\n"}) @@ -85,7 +85,7 @@ module Heroku describe 'beta' do before do - zip = File.read(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) + zip = IO.binread(File.expand_path('../../fixtures/heroku-client-3.9.7.zip', __FILE__)) Excon.stub({:host => 'toolbelt.heroku.com', :path => '/download/beta-zip'}, {:body => zip}) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 727bd08b8..d8a421a89 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,10 @@ -$stdin = File.new("/dev/null") +require "heroku/helpers" + +if (Heroku::Helpers.running_on_windows?) + $stdin = File.new("nul") +else + $stdin = File.new("/dev/null") +end require "rubygems" From 593c7c3e24c4e67112fdd855d8297215e503b415 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Tue, 17 Nov 2015 09:50:08 -0600 Subject: [PATCH 798/952] Making the required init-wine task more obvious --- RELEASE-FULL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/RELEASE-FULL.md b/RELEASE-FULL.md index 07a5682f7..2594741e5 100644 --- a/RELEASE-FULL.md +++ b/RELEASE-FULL.md @@ -53,8 +53,7 @@ building; run the `exe:pvk` task for that. You'll have to ask the right person for the passphrase to the key. -You then need to initialize a custom wine build environment. The `exe:init-wine` -task will do that for you. +* Initialize wine: `bundle exec rake exe:init-wine` To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. To release: `bundle exec rake pkg:release`. From 16f8601639385b04fdc151b2a31ecca32b22b1e6 Mon Sep 17 00:00:00 2001 From: Josh Sullivan Date: Tue, 17 Nov 2015 13:28:03 -0800 Subject: [PATCH 799/952] spaces: don't use platform api dogwood variant The platform api dogwood variant is getting merged into prod v3. We don't need to make calls to the platform api requesting the variant for dogwood-specific properties of organizations, regions, or spaces. Calls to prod v3 will return the same properties now. --- ...nizations_apps_v3_dogwood.rb => organizations_apps.rb} | 4 ++-- lib/heroku/api/{spaces_v3_dogwood.rb => spaces.rb} | 4 ++-- lib/heroku/command/apps.rb | 4 ++-- lib/heroku/command/base.rb | 4 ++-- spec/heroku/command/apps_spec.rb | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) rename lib/heroku/api/{organizations_apps_v3_dogwood.rb => organizations_apps.rb} (66%) rename lib/heroku/api/{spaces_v3_dogwood.rb => spaces.rb} (62%) diff --git a/lib/heroku/api/organizations_apps_v3_dogwood.rb b/lib/heroku/api/organizations_apps.rb similarity index 66% rename from lib/heroku/api/organizations_apps_v3_dogwood.rb rename to lib/heroku/api/organizations_apps.rb index fc5ecf62c..26b9fcadf 100644 --- a/lib/heroku/api/organizations_apps_v3_dogwood.rb +++ b/lib/heroku/api/organizations_apps.rb @@ -1,13 +1,13 @@ module Heroku class API - def post_organizations_app_v3_dogwood(params={}) + def post_organizations_app(params={}) request( :method => :post, :body => Heroku::Helpers.json_encode(params), :expects => 201, :path => "/organizations/apps", :headers => { - "Accept" => "application/vnd.heroku+json; version=3.dogwood" + "Accept" => "application/vnd.heroku+json" } ) end diff --git a/lib/heroku/api/spaces_v3_dogwood.rb b/lib/heroku/api/spaces.rb similarity index 62% rename from lib/heroku/api/spaces_v3_dogwood.rb rename to lib/heroku/api/spaces.rb index 5a4c25c45..ca9eef18e 100644 --- a/lib/heroku/api/spaces_v3_dogwood.rb +++ b/lib/heroku/api/spaces.rb @@ -1,12 +1,12 @@ module Heroku class API - def get_space_v3_dogwood(space_identity) + def get_space(space_identity) request( :method => :get, :expects => [200], :path => "/spaces/#{space_identity}", :headers => { - "Accept" => "application/vnd.heroku+json; version=3.dogwood" + "Accept" => "application/vnd.heroku+json" } ) end diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index 49d8e6fcc..f26876278 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -1,6 +1,6 @@ require "heroku/command/base" require "heroku/command/stack" -require "heroku/api/organizations_apps_v3_dogwood" +require "heroku/api/organizations_apps" # manage apps (create, destroy) # @@ -124,7 +124,7 @@ def create } info = if options[:space] - api.post_organizations_app_v3_dogwood(params).body + api.post_organizations_app(params).body elsif org org_api.post_app(params, org).body else diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 6b2729a5f..1f7c497cd 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -3,7 +3,7 @@ require "heroku/client/rendezvous" require "heroku/client/organizations" require "heroku/command" -require "heroku/api/spaces_v3_dogwood" +require "heroku/api/spaces" class Heroku::Command::Base include Heroku::Helpers @@ -44,7 +44,7 @@ def org @org ||= if options[:space].is_a?(String) validate_space_xor_org! - api.get_space_v3_dogwood(options[:space]).body['organization']['name'] + api.get_space(options[:space]).body['organization']['name'] elsif options[:org].is_a?(String) options[:org] elsif options[:personal] || @nil diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index b3aaf4ade..2e9608232 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -6,7 +6,7 @@ module Heroku::Command before(:each) do stub_core - stub_get_space_v3_dogwood + stub_get_space stub_organizations ENV.delete('HEROKU_ORGANIZATION') end @@ -111,7 +111,7 @@ module Heroku::Command context "with a space" do shared_examples "create in a space" do Excon.stub( - :headers => { 'Accept' => 'application/vnd.heroku+json; version=3.dogwood'}, + :headers => {'Accept' => 'application/vnd.heroku+json'}, :method => :post, :path => '/organizations/apps') do { @@ -471,9 +471,9 @@ module Heroku::Command end end - def stub_get_space_v3_dogwood + def stub_get_space Excon.stub( - :headers => { 'Accept' => 'application/vnd.heroku+json; version=3.dogwood' }, + :headers => {'Accept' => 'application/vnd.heroku+json'}, :method => :get, :path => '/spaces/test-space') do { From 2aa04652c97e78ab0a67e5c10fd36a37b7d371f2 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 18 Nov 2015 15:12:07 -0600 Subject: [PATCH 800/952] Freeze net-ssh to 2.9.2 to preserve 1.9.3 support --- Gemfile.lock | 1 + heroku.gemspec | 1 + 2 files changed, 2 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 3db883ad6..6eb9f6370 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) + net-ssh (= 2.9.2) net-ssh-gateway (= 1.2.0) netrc (= 0.10.3) rest-client (= 1.6.8) diff --git a/heroku.gemspec b/heroku.gemspec index 0eae472cc..3aff6c37d 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -28,4 +28,5 @@ Gem::Specification.new do |gem| gem.add_dependency "rubyzip", "1.1.7" gem.add_dependency "multi_json", "1.11.2" gem.add_dependency "net-ssh-gateway", "1.2.0" + gem.add_dependency "net-ssh", "2.9.2" # freeze net-ssh to 2.9.2 to preserve ruby 1.9.3 support end From d0acc97a5c0130910c8bb2a1b6b21f8c0ebc2432 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 18 Nov 2015 15:17:32 -0600 Subject: [PATCH 801/952] v3.42.22 --- CHANGELOG | 12 ++++++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index bf6ed520f..710727377 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,15 @@ +3.42.22 2015-11-18 +================== +prefix update messaging +update v4 plugins when running plugins:update +Adding verification test for config:get not found +use $XDG_DATA_HOME if available +Adding test for config:get --shell not found +Use attachment SSO url if it can be determined +Fixing tests so they can run under windows +Making the required init-wine task more obvious +Freeze net-ssh to 2.9.2 to preserve 1.9.3 support + 3.42.21 2015-10-30 ================== Remove protocol prefix for buildpack URN diff --git a/Gemfile.lock b/Gemfile.lock index 6eb9f6370..4da883d1d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.21) + heroku (3.42.22) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index cceb51527..f781642ac 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.21" + VERSION = "3.42.22" end From 1aedda918c618d9da3660999f1a47745d20095c1 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 18 Nov 2015 16:07:55 -0600 Subject: [PATCH 802/952] Update RELEASE-FULL.md OSX notes --- RELEASE-FULL.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE-FULL.md b/RELEASE-FULL.md index 2594741e5..55bec8bfb 100644 --- a/RELEASE-FULL.md +++ b/RELEASE-FULL.md @@ -9,6 +9,12 @@ Prerequisites: * OSX * Heroku Developer ID Installer Certificate in Keychain + * `gpg --decrypt-files resources/pkg/certificate.p12.gpg` + * Enter OSX .p12 Certificate password (LastPass Shared CLI Secure Note) + * open resources/pkg/certificate.p12 + * There is no password on certificate.p12 (it was GPG encrypted instead) + * rm resources/pkg/certificate.p12 (you do not need it anymore) + * `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` To build for testing: `bundle exec rake pkg:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.pkg`. From e73b4b22f54a4db84701cb41f643ad7182be261d Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Nov 2015 14:17:00 -0800 Subject: [PATCH 803/952] added encrypted windows pfx file --- .../exe/heroku-code-signing-certificate.pfx.gpg | Bin 0 -> 4628 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/exe/heroku-code-signing-certificate.pfx.gpg diff --git a/resources/exe/heroku-code-signing-certificate.pfx.gpg b/resources/exe/heroku-code-signing-certificate.pfx.gpg new file mode 100644 index 0000000000000000000000000000000000000000..3b4a67de9b7b9d4190576773cca71478f15ba4af GIT binary patch literal 4628 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 KY?A*@7y)s}`sZK( literal 0 HcmV?d00001 From db933e8741780ceed0950180ef0ab46159dd5d2f Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Nov 2015 16:20:11 -0800 Subject: [PATCH 804/952] switch to osslsigncode to fix windows installs --- .gitignore | 2 +- RELEASE-FULL.md | 23 ++------ .../exe/heroku-codesign-cert.encrypted.pvk | Bin 1212 -> 0 bytes ...e.pfx.gpg => heroku-codesign-cert.pfx.gpg} | Bin resources/exe/heroku-codesign-cert.spc | Bin 1745 -> 0 bytes resources/exe/heroku.iss | 1 - tasks/exe.rake | 50 ++++-------------- 7 files changed, 13 insertions(+), 63 deletions(-) delete mode 100644 resources/exe/heroku-codesign-cert.encrypted.pvk rename resources/exe/{heroku-code-signing-certificate.pfx.gpg => heroku-codesign-cert.pfx.gpg} (100%) delete mode 100644 resources/exe/heroku-codesign-cert.spc diff --git a/.gitignore b/.gitignore index eebb386cf..e824b39ae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,4 @@ /vendor /.rbenv-version /.cache -/resources/exe/heroku-codesign-cert.pvk +/resources/exe/heroku-codesign-cert.pfx diff --git a/RELEASE-FULL.md b/RELEASE-FULL.md index 55bec8bfb..72fef7cc6 100644 --- a/RELEASE-FULL.md +++ b/RELEASE-FULL.md @@ -27,8 +27,9 @@ This is run not from a Windows machine, but from a UNIX machine with Wine. Mac Prerequisites: * Heroku Developer ID Installer Certificate in Keychain -* `HEROKU_RELEASE_ACCESS` and `HEROKU_RELEASE_SECRET` +* `HEROKU_RELEASE_ACCESS`, `HEROKU_RELEASE_SECRET`, `HEROKU_WINDOWS_SIGNING_PASS` (from LastPass) * Install [XQuartz](http://xquartz.macosforge.org/) manually, or via the terminal (restart required): +* `brew install osslsigncode` ```sh curl -O# http://xquartz-dl.macosforge.org/SL/XQuartz-2.7.6.dmg @@ -40,25 +41,7 @@ rm XQuartz-2.7.6.dmg * `/opt/X11/bin` should be in your `$PATH` so `Xvfb` can be started. * Install wine: `brew install wine` -* The pvk file: - -The certificate and private key for code signing are in the repo in: - -> dist/resources/exe/heroku-codesign-cert* - -which is in the format mono signcode wants. - -The pvk file is encrypted. If you want the build not to prompt you for -its passphrase, you'll need to decrypt it. See the `exe:pvk-nocrypt` task. - -Bewake the openssl version on the Mac doesn't work with `exe:pvk-nocrypt`. -See comments on the source code for details and solution. - -If you wanna leave the key encrypted, you still have to link it before -building; run the `exe:pvk` task for that. - -You'll have to ask the right person for the passphrase to the key. - +* The pfx file decrypted from `resources/exe/heroku-codesign-cert.pfx.gpg` (password in LastPass) * Initialize wine: `bundle exec rake exe:init-wine` To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. diff --git a/resources/exe/heroku-codesign-cert.encrypted.pvk b/resources/exe/heroku-codesign-cert.encrypted.pvk deleted file mode 100644 index d8e6ca57386a3bc2bb37df27c41f24ab3f06fbf2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1212 zcmV;t1Vj5C@wKo300001000010000G0001#1ONckfc6w8qCo(#OO3)qPMVGf0ssI2 zqyPX9W4*UY?L zgmdke45kI%gJ5B(jDa`)%?_8J1wn`+vxVwbIn@$H()CPOSoUsl7xgmUZR`SfgTzTQ z48UywV${B}s|CNH2Bvg#=y3*>Z5b--w)_FAe}nNIMUa#$>og4L78b$-N|Lw_9;6;Q zlT_c5tA!W#Ccc*D`^3dhk4XM==lw%$d*Zc%X&^{*+RACbwM34;OO zG?C;0rL+aS^+zLi6J;0SC^_zDQGMyyqAYdVlCS|dBRf z_)GKM8q$HT5N4uQp*~pQV7_}l(TUMo4D@k^*oB%GeKTa1rY&sgzcLKJA}W3u_59Wu z7Y4&FOf+Ni6{8;p7%OT~Via2YY+B>0g)H(@CEwWDYnbBOC=IwFE#zy+yh&~|hl*fa zLecWqVlBqR0VY6tM!|+}4Sr<6)KYt=1UMWD5%_`_fL*=+;>G*DLQ=dG8C zz{;VgJfR!)*J+{CHn`;7N!Xv^3Tyrwr%cSYskXHw0ieCX-=aC1^0Vtizv79Dn%0)n ztY4O50)nZ`vz)O-RmueMETFPqEs3zk&vFC6725IPXGTnad6_9@BKP60Qm?|WO%Qz) zH~kHjL+UQw=uCRldrgc@P1u+pUIVdY^Un{^lJSxc;6m$0tuiAG#&M*IU2j!F$tmyQ zow@A4H`d)kJZm3$cl{D)%z5Nyc$rhd#CniYT^it9sL{t}AM$B4M-+hh?UcGCaNK8g z6fA46&^|-^o+iQN;njGU-f((5E+AD4Jdl;zEj3x#XN?)vcj?lr>dNRVhP$xT2!;!( zAS6=AdSCC%6jME{-{XPLp#tcGa@|W|a=Y6q;>TTDPcc^1b}c1Cp{cvF8}vCJ zWv=LiHQW1GBt6ZqF2fyM!P3_sTS6dtmh1_{Xvc=6>0M&@_zwE2$~dbh=c1%l2_4if z^SS=l^wuZ8L8TGn7b~;|T}g;K?&a7Y4wm$%`xLDdmZ0nbWPs{m?l~0RF(wc4i5omp z+C{8jWUqV13Hk=xyx3_ diff --git a/resources/exe/heroku-code-signing-certificate.pfx.gpg b/resources/exe/heroku-codesign-cert.pfx.gpg similarity index 100% rename from resources/exe/heroku-code-signing-certificate.pfx.gpg rename to resources/exe/heroku-codesign-cert.pfx.gpg diff --git a/resources/exe/heroku-codesign-cert.spc b/resources/exe/heroku-codesign-cert.spc deleted file mode 100644 index a5c57f3d5e547f5a54f34b12f562b087faba3418..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1745 zcma)6e^3-v9Dloe+`)0jk6GkU{IQ|fWZd0b5KtVmNG1<8Nlr7R$>hRG(=kodniNZHhDw<-g#1Mg1-%CxLH*OtytnWB`F_6N z_jzx>`-P&kO&T`KYxMRh2^r}Vr6-^$Z6OJQ1u*;{4}m;B_@=#2%==up+&67khE>xeixZsNZmWVPw*q{tCXbPscE{%dn+4 z`tS?FV3oBsEis3@rdmb&JKBtMk5(7$O=~~3#FM%Bs;YkTkwdvB=gzK|ZJ6G9%^I%jY{Mebo&N?#pK^(MzkqAF9{(sbJy#h=cyp=-Usw%uM9^vNh+k zwmh%+c>l7VNxN?r2#&3XqMN()?Olsh^n;DfeLvoa2s2OUco6nj$bhADVxK8{AfS7w z8pwXU`R2t9MSFj0O3T0VZg$;K&9@eD@XnIRO2@s4C7D0maOr*5PRt`I0uXyeYAzJ1 zR5%UY_>}3AI2gM&W_8KMCz?K2oyb|$d2_+l#=fk#Qj_5nOo))gN@D!HYe&Zlb09OL zf76-xiVM7MemRW5?nr_dlt?I2wsxq_rX)%fP%aI9Qi^I(Mp4j>Nz){D_$o~Cu|BO= zUE=Y$GPGJ=u&0cxJ1sX{WNO~6PKGSYX%OYVL9_oykVu@y+cFZ72%wLD42@=K*ehNm z;r+`_5y;#JGlC*XB33h^>EFMC*6p^dd6!!)m|ae{&Epg*5w#S>7mr4h$a-bRY)ycu+J(F`q*o9<5yn z;Sd9_$7aQr0mWF+21fVRSeiYs7oujCe1+2R@sCPJrj(^I`_pf;pJM zpe%TIY+FX~;j?PsIx%?71R*Jh9ylXtL&6(3^3j~U+cmBxVD2c3^gbOdHxBnV_c)?9 z*xyFlTaE!Yw~^k4TN|oC4^-e%qH&p%5R7dvsM(_SLICP&g7!Dzo3qB--AP|=zPEEt ze|W2?@mqH{);yFQTOU|jc*ks07}c4Bbrk+QS5`jjs$|3*)3RN~HMf0FOq4eZ~}x0pWh&pdMF qY;kw*E>*8Io>kb}uGc)8=X|a=uc7eQmW3U0tGDn^88 "zip:build" do |exe_task| # this sets the version and the output filename File.write("#{installer_path}/heroku.iss", ERB.new(File.read(resource("exe/heroku.iss"))).result(binding)) - # the codesign command used by inno to sign the installer and uninstaller - sign_cmd = 'c:\windows\mono\mono-2.0\lib\mono\4.5\signcode.exe' + %Q[ - -spc "#{windows_path(resource('exe/heroku-codesign-cert.spc'))}" - -v "#{windows_path(resource('exe/heroku-codesign-cert.pvk'))}" - -a sha1 -$ commercial - -n "Heroku Toolbelt" - $f ]. # $f gets replaced by iscc with the path to the file it wants to compile - gsub("\n", ' '). # everything on a single line now - gsub('"', '$q') # iscc requires quotes to be escaped this way, don't ask - # compile installer under wine! setup_wine_env - system 'wine', 'C:\inno\ISCC.exe', - "/Smono-signcode=#{sign_cmd}", '/qp', - windows_path("#{installer_path}/heroku.iss") + system 'wine', 'C:\inno\ISCC.exe', windows_path("#{installer_path}/heroku.iss") cleanup_after_wine # move final installer from build_path to pkg dir mv File.basename(exe_task.name), exe_task.name + + # sign executable + system "osslsigncode -pkcs12 #{resource('exe/heroku-codesign-cert.pfx')} \ + -pass '#{ENV['HEROKU_WINDOWS_SIGNING_PASS']}' \ + -n 'Heroku Toolbelt' \ + -i https://toolbelt.heroku.com/ \ + -in #{exe_task.name} \ + -out #{exe_task.name}" end end @@ -126,31 +122,3 @@ task "exe:init-wine" do system "wine #{isetup_path} /verysilent /suppressmsgboxes /nocancel /norestart /noicons /dir=c:\\inno" cleanup_after_wine end - -# Mono's signcode tool can't take the private key passphrase non-interactively (i.e. read file, or as a parameter), so -# in order to run the build non-interactively we have to use a passphrase-less key. To keep the private key secure, the -# key that comes from the repository is encrypted. You can either run exe:build and type in the passphrase manually -# (twice!), or decode it for good with this task. -# -# Ensure your build environment is secure before leaving an unencrypted private key lying around. -# -# Additionally, Mac OS X's default openssl, as of Mavericks, is 0.9.8y, which doesn't support the pvk format. The 1.0.x -# tree does, and you can install it via homebrew (brew install openssl), but it's keg-only, so it'll not be in your -# PATH. You could `brew link` it, but it's safer to leave it alone. Instead, you can pass the full path to the openssl -# binary to be used via the OPENSSL_PATH environment variable: -# -# OPENSSL_PATH=`brew --prefix openssl`/bin/openssl rake exe:pvk-nocrypt -desc "Remove passphrase from heroku-codesign-cert.pvk; see source comments" -task "exe:pvk-nocrypt" do - openssl = (ENV["OPENSSL_PATH"] || "openssl").shellescape - version = `#{openssl} version`.chomp - keyfile_in = resource('exe/heroku-codesign-cert.encrypted.pvk').shellescape - keyfile_out = resource('exe/heroku-codesign-cert.pvk').shellescape - raise "OpenSSL version should be 1.0.x; instead got: #{version}" if version !~ /^OpenSSL 1\./ - system "#{openssl} rsa -inform PVK -outform PVK -pvk-none -in #{keyfile_in} -out #{keyfile_out}" -end - -desc "Link the encrypted pvk" -task "exe:pvk" do - symlink resource("exe/heroku-codesign-cert.encrypted.pvk"), resource("exe/heroku-codesign-cert.pvk") -end From 2e90d508f8a1a70701b714fee0ef76f309517169 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Nov 2015 16:42:09 -0800 Subject: [PATCH 805/952] upgrade ruby to 2.2.3 and git to 2.6.3 on windows --- resources/exe/heroku.iss | 12 ++++++------ tasks/exe.rake | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index da7e4eea7..940657874 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -24,13 +24,13 @@ Name: custom; Description: "Custom Installation"; flags: iscustom [Components] Name: "toolbelt"; Description: "Heroku Toolbelt"; Types: "client custom" Name: "toolbelt/client"; Description: "Heroku Client"; Types: "client custom"; Flags: fixed -Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git.exe')" -Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git.exe')" +Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git-2.6.3.exe')" +Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git-2.6.3.exe')" [Files] Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" -Source: "installers\rubyinstaller.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" -Source: "installers\git.exe"; DestDir: "{tmp}"; Components: "toolbelt/git" +Source: "installers\rubyinstaller-2.2.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" +Source: "installers\git-2.6.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/git" [Registry] Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "HerokuPath"; \ @@ -41,9 +41,9 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen ValueData: "{olddata};{pf}\git\cmd"; Check: NeedsAddPath(ExpandConstant('{pf}\git\cmd')) [Run] -Filename: "{tmp}\rubyinstaller.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-1.9.3"""; \ +Filename: "{tmp}\rubyinstaller-2.2.3.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-1.9.3"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" -Filename: "{tmp}\git.exe"; Parameters: "/silent /nocancel /noicons"; \ +Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/silent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" [UninstallDelete] diff --git a/tasks/exe.rake b/tasks/exe.rake index e716c64bd..7e67074fb 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -63,7 +63,7 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| # gather the ruby and git installers, downlading from s3 mkdir "#{installer_path}/installers" cd "#{installer_path}/installers" do - ["rubyinstaller.exe", "git.exe"].each { |i| cp cache_file_from_bucket(i), i } + ["rubyinstaller-2.2.3.exe", "git-2.6.3.exe"].each { |i| cp cache_file_from_bucket(i), i } end # add windows helper executables to the heroku cli From f0acbeac1bb08add22d64bf9116b4a595c7ef8eb Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Nov 2015 16:42:45 -0800 Subject: [PATCH 806/952] small copy tweak --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 527747ee4..82e6b22db 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -141,7 +141,7 @@ def self.setup File.delete bin raise 'SHA mismatch for heroku-cli' end - $stderr.puts " done.\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" + $stderr.puts " done\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" version end From 0ee479e181afc72dccd17643db13c3c9c17b66e2 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 18 Nov 2015 12:58:03 -0800 Subject: [PATCH 807/952] clear up unknown database error --- lib/heroku/helpers/heroku_postgresql.rb | 6 +++++- spec/heroku/helpers/heroku_postgresql_spec.rb | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/heroku/helpers/heroku_postgresql.rb b/lib/heroku/helpers/heroku_postgresql.rb index c2bec6aa8..9a5d2ebc2 100644 --- a/lib/heroku/helpers/heroku_postgresql.rb +++ b/lib/heroku/helpers/heroku_postgresql.rb @@ -221,7 +221,11 @@ def hpg_resolve(name, default=nil) end if found_attachment.nil? - error("Unknown database#{': ' + name unless name.empty?}. Valid options are: #{hpg_databases.keys.sort.join(", ")}") + if name.empty? + error("No default database configured in DATABASE_URL. Valid alternatives are: #{hpg_databases.keys.sort.join(", ")}") + else + error("Unknown database#{': ' + name unless name.empty?}. Valid options are: #{hpg_databases.keys.sort.join(", ")}") + end end return found_attachment diff --git a/spec/heroku/helpers/heroku_postgresql_spec.rb b/spec/heroku/helpers/heroku_postgresql_spec.rb index f3cfd8fd2..92a27abf8 100644 --- a/spec/heroku/helpers/heroku_postgresql_spec.rb +++ b/spec/heroku/helpers/heroku_postgresql_spec.rb @@ -176,7 +176,7 @@ context "default" do it "errors if there is no default" do - expect(@resolver).to receive(:error).with("Unknown database. Valid options are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("No default database configured in DATABASE_URL. Valid alternatives are: DATABASE_URL, HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") @resolver.resolve(nil) end @@ -194,13 +194,13 @@ it 'throws an error if given an empty string and asked for the default and there is no default' do app_config_vars.delete 'DATABASE_URL' - expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("No default database configured in DATABASE_URL. Valid alternatives are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end it 'throws an error if given an empty string and asked for the default and the default doesnt match' do app_config_vars['DATABASE_URL'] = 'something different' - expect(@resolver).to receive(:error).with("Unknown database. Valid options are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") + expect(@resolver).to receive(:error).with("No default database configured in DATABASE_URL. Valid alternatives are: HEROKU_POSTGRESQL_BLACK_URL, HEROKU_POSTGRESQL_IVORY_URL") att = @resolver.resolve('', "DATABASE_URL") end end From 33d50d8ab58d683426fcfd3ddbdce37fe51c58c7 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 19 Nov 2015 09:57:47 -0600 Subject: [PATCH 808/952] Install updated ruby to ruby-2.2.3 --- resources/exe/heroku | 2 +- resources/exe/heroku.bat | 2 +- resources/exe/heroku.iss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/exe/heroku b/resources/exe/heroku index 7a7fd4ce5..7dc08a101 100755 --- a/resources/exe/heroku +++ b/resources/exe/heroku @@ -1,6 +1,6 @@ #!/bin/sh # find embedded ruby relative to script -bindir=`cd -P "${0%/*}/../ruby-1.9.3/bin" 2>/dev/null; pwd` +bindir=`cd -P "${0%/*}/../ruby-2.2.3/bin" 2>/dev/null; pwd` exec "$bindir/ruby" -x "$0" "$@" #!/usr/bin/env ruby diff --git a/resources/exe/heroku.bat b/resources/exe/heroku.bat index f4f8bacc8..b1d92db08 100644 --- a/resources/exe/heroku.bat +++ b/resources/exe/heroku.bat @@ -3,7 +3,7 @@ @SETLOCAL :: Add bundled ruby version to the PATH, use HerokuPath as starting point -@SET HEROKU_RUBY="%HerokuPath%\ruby-1.9.3\bin" +@SET HEROKU_RUBY="%HerokuPath%\ruby-2.2.3\bin" @SET PATH=%HEROKU_RUBY%;%PATH%;%ProgramFiles(x86)%\Git\bin :: Invoke 'heroku' (the calling script) as argument to ruby. diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 940657874..11ff655f5 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -41,7 +41,7 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen ValueData: "{olddata};{pf}\git\cmd"; Check: NeedsAddPath(ExpandConstant('{pf}\git\cmd')) [Run] -Filename: "{tmp}\rubyinstaller-2.2.3.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-1.9.3"""; \ +Filename: "{tmp}\rubyinstaller-2.2.3.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.2.3"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/silent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" From 4c1491a3a6167e385acff1d4e06e720f5c853f52 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 19 Nov 2015 11:25:38 -0600 Subject: [PATCH 809/952] Remove rendezvous spec that fails when not a tty * rendezvous is no longer used by the core ruby libraries leaving the lib & other tests in place for possible ruby plugin deps --- spec/heroku/client/rendezvous_spec.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/heroku/client/rendezvous_spec.rb b/spec/heroku/client/rendezvous_spec.rb index 1cc73f7b8..dd9f9f896 100644 --- a/spec/heroku/client/rendezvous_spec.rb +++ b/spec/heroku/client/rendezvous_spec.rb @@ -17,9 +17,6 @@ it "an empty string" do expect(@rendezvous.send(:fixup, "")).to eq "" end - it "hash" do - expect(@rendezvous.send(:fixup, { :x => :y })).to eq({ :x => :y }) - end it "default English UTF-8 data" do expect(@rendezvous.send(:fixup, "heroku")).to eq "heroku" end From 2be1bc163d37193b038ea56aec43cc25c076503b Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 19 Nov 2015 11:41:04 -0600 Subject: [PATCH 810/952] Downgrade ruby 2.2.3 to 2.1.7 for net-ssh fix * fixes cannot load such file -- dl/import (LoadError) on Windows --- resources/exe/heroku | 2 +- resources/exe/heroku.bat | 2 +- resources/exe/heroku.iss | 4 ++-- tasks/exe.rake | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/exe/heroku b/resources/exe/heroku index 7dc08a101..77eddbf68 100755 --- a/resources/exe/heroku +++ b/resources/exe/heroku @@ -1,6 +1,6 @@ #!/bin/sh # find embedded ruby relative to script -bindir=`cd -P "${0%/*}/../ruby-2.2.3/bin" 2>/dev/null; pwd` +bindir=`cd -P "${0%/*}/../ruby-2.1.7/bin" 2>/dev/null; pwd` exec "$bindir/ruby" -x "$0" "$@" #!/usr/bin/env ruby diff --git a/resources/exe/heroku.bat b/resources/exe/heroku.bat index b1d92db08..df6d00692 100644 --- a/resources/exe/heroku.bat +++ b/resources/exe/heroku.bat @@ -3,7 +3,7 @@ @SETLOCAL :: Add bundled ruby version to the PATH, use HerokuPath as starting point -@SET HEROKU_RUBY="%HerokuPath%\ruby-2.2.3\bin" +@SET HEROKU_RUBY="%HerokuPath%\ruby-2.1.7\bin" @SET PATH=%HEROKU_RUBY%;%PATH%;%ProgramFiles(x86)%\Git\bin :: Invoke 'heroku' (the calling script) as argument to ruby. diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 11ff655f5..6686e9eff 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -29,7 +29,7 @@ Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('gi [Files] Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" -Source: "installers\rubyinstaller-2.2.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" +Source: "installers\rubyinstaller-2.1.7.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" Source: "installers\git-2.6.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/git" [Registry] @@ -41,7 +41,7 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen ValueData: "{olddata};{pf}\git\cmd"; Check: NeedsAddPath(ExpandConstant('{pf}\git\cmd')) [Run] -Filename: "{tmp}\rubyinstaller-2.2.3.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.2.3"""; \ +Filename: "{tmp}\rubyinstaller-2.1.7.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.1.7"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/silent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" diff --git a/tasks/exe.rake b/tasks/exe.rake index 7e67074fb..d11cd2b0c 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -63,7 +63,7 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| # gather the ruby and git installers, downlading from s3 mkdir "#{installer_path}/installers" cd "#{installer_path}/installers" do - ["rubyinstaller-2.2.3.exe", "git-2.6.3.exe"].each { |i| cp cache_file_from_bucket(i), i } + ["rubyinstaller-2.1.7.exe", "git-2.6.3.exe"].each { |i| cp cache_file_from_bucket(i), i } end # add windows helper executables to the heroku cli From 1a2e2151cfb0f86aa7a6b3830ba9c29c3ff0568f Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 19 Nov 2015 16:50:35 -0600 Subject: [PATCH 811/952] Add plugins:command to track progress --- lib/heroku/command/plugins.rb | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index ec7f84abb..88d29be85 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -1,4 +1,5 @@ require "heroku/command/base" +require "heroku/jsplugin" module Heroku::Command @@ -120,6 +121,41 @@ def link Heroku::JSPlugin.run('plugins', 'link', ARGV[1..-1]) end + # HIDDEN: plugins:commands + # Prints a table of commands and location + def commands + ruby_cmd = Heroku::Command.commands.inject({}) {|h, (cmd, _)| h[cmd] = {:type => 'ruby'} ; h} + + go_and_node_cmd = Heroku::JSPlugin.commands_info['commands'].inject({}) do |h, command| + cmd = command['command'] ? "#{command['topic']}:#{command['command']}" : command['topic'] + if command['plugin'] == '' + h[cmd] = {:type => 'go'} + else + h[cmd] = {:type => 'node', :plugin => command['plugin']} + end + h + end + + all_cmd = {} + all_cmd.merge!(ruby_cmd) + all_cmd.merge!(go_and_node_cmd) + all_cmd.each {|(cmd_str, cmd)| cmd[:command] = cmd_str} + + sorted_cmd = all_cmd.sort do |a,b| + if a[1][:type] == b[1][:type] + a[0] <=> b[0] + else + a[1][:type] <=> b[1][:type] + end + end + + display_table(sorted_cmd.map{|cmd| cmd[1]}, [:command, :type, :plugin], ["Command", "Type", "Plugin"]) + display("============") + + counts = all_cmd.inject(Hash.new(0)) {|h, (_, cmd)| h[cmd[:type]] += 1; h} + counts.keys.sort.each {|type| display("% #{type}: #{(counts[type].to_f / all_cmd.size).round(2)}") } + end + private def js_plugin_install(name) From 2e81f79fb4b6f895d43c4049f2608d2fd8c1a779 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 20 Nov 2015 12:49:26 -0600 Subject: [PATCH 812/952] Change plugins sort to just command name --- lib/heroku/command/plugins.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 88d29be85..e9e673e4b 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -141,13 +141,7 @@ def commands all_cmd.merge!(go_and_node_cmd) all_cmd.each {|(cmd_str, cmd)| cmd[:command] = cmd_str} - sorted_cmd = all_cmd.sort do |a,b| - if a[1][:type] == b[1][:type] - a[0] <=> b[0] - else - a[1][:type] <=> b[1][:type] - end - end + sorted_cmd = all_cmd.sort { |a,b| a[0] <=> b[0] } display_table(sorted_cmd.map{|cmd| cmd[1]}, [:command, :type, :plugin], ["Command", "Type", "Plugin"]) display("============") From 1b4b597312cc5a665192cd54e4967741280de7d2 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 20 Nov 2015 15:40:56 -0600 Subject: [PATCH 813/952] Fix default command detection --- lib/heroku/command/plugins.rb | 37 +++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index e9e673e4b..29a6101e4 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -124,22 +124,15 @@ def link # HIDDEN: plugins:commands # Prints a table of commands and location def commands - ruby_cmd = Heroku::Command.commands.inject({}) {|h, (cmd, _)| h[cmd] = {:type => 'ruby'} ; h} - - go_and_node_cmd = Heroku::JSPlugin.commands_info['commands'].inject({}) do |h, command| - cmd = command['command'] ? "#{command['topic']}:#{command['command']}" : command['topic'] - if command['plugin'] == '' - h[cmd] = {:type => 'go'} - else - h[cmd] = {:type => 'node', :plugin => command['plugin']} - end - h - end + ruby_cmd = Heroku::Command.commands.inject({}) {|h, (cmd, command)| h[cmd] = command_to_hash('ruby', cmd, command) ; h} + commands = Heroku::JSPlugin.commands_info['commands'] + node_cmd = command_list_to_hash(commands.select {|command| command['plugin'] != ''}, 'node') + go_cmd = command_list_to_hash(commands.select {|command| command['plugin'] == ''}, 'go') all_cmd = {} all_cmd.merge!(ruby_cmd) - all_cmd.merge!(go_and_node_cmd) - all_cmd.each {|(cmd_str, cmd)| cmd[:command] = cmd_str} + all_cmd.merge!(node_cmd) + all_cmd.merge!(go_cmd) sorted_cmd = all_cmd.sort { |a,b| a[0] <=> b[0] } @@ -152,6 +145,24 @@ def commands private + def command_to_hash(type, cmd, command) + command_hash = {:type => type, :command => cmd} + command_hash[:plugin] = command['plugin'] if command['plugin'] && command['plugin'] != '' + command_hash + end + + def command_list_to_hash(commands, type) + commands.inject({}) do |h, command| + cmd = command['command'] ? "#{command['topic']}:#{command['command']}" : command['topic'] + h[cmd] = command_to_hash(type, cmd, command) + if command['default'] + cmd = command['topic'] + h[cmd] = command_to_hash(type, cmd, command) + end + h + end + end + def js_plugin_install(name) Heroku::JSPlugin.install(name, force: true) end From e2af3f0ebdf337ef82634327758a2ccdf8d43d9c Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 20 Nov 2015 16:23:29 -0600 Subject: [PATCH 814/952] Add --csv flag to format plugins:commands --- lib/heroku/command/plugins.rb | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 29a6101e4..e37dda108 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -1,5 +1,6 @@ require "heroku/command/base" require "heroku/jsplugin" +require "csv" module Heroku::Command @@ -122,8 +123,13 @@ def link end # HIDDEN: plugins:commands + # # Prints a table of commands and location + # + # -c, --csv # Show with csv formatting def commands + validate_arguments! + ruby_cmd = Heroku::Command.commands.inject({}) {|h, (cmd, command)| h[cmd] = command_to_hash('ruby', cmd, command) ; h} commands = Heroku::JSPlugin.commands_info['commands'] node_cmd = command_list_to_hash(commands.select {|command| command['plugin'] != ''}, 'node') @@ -134,13 +140,32 @@ def commands all_cmd.merge!(node_cmd) all_cmd.merge!(go_cmd) - sorted_cmd = all_cmd.sort { |a,b| a[0] <=> b[0] } + sorted_cmd = all_cmd.sort { |a,b| a[0] <=> b[0] }.map{|cmd| cmd[1]} - display_table(sorted_cmd.map{|cmd| cmd[1]}, [:command, :type, :plugin], ["Command", "Type", "Plugin"]) - display("============") + attrs = [:command, :type, :plugin] + header = attrs.map{|attr| attr.to_s.capitalize} counts = all_cmd.inject(Hash.new(0)) {|h, (_, cmd)| h[cmd[:type]] += 1; h} - counts.keys.sort.each {|type| display("% #{type}: #{(counts[type].to_f / all_cmd.size).round(2)}") } + type_and_percentage = counts.keys.sort.map{|type| [type, (counts[type].to_f / all_cmd.size).round(2)]} + + if options[:csv] + csv_str = CSV.generate do |csv| + csv << header + sorted_cmd.each {|cmd| csv << attrs.map{|attr| cmd[attr]}} + + csv << [] + csv << ['Type', 'Percentage'] + type_and_percentage.each {|type| + csv << type + } + end + display(csv_str) + else + display_table(sorted_cmd, attrs, header) + display("============") + + type_and_percentage.each {|type| display("% #{type[0]}: #{type[1]}")} + end end private From 3f8fe17b2f5cb70ac58731886e09e29ee64d476f Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 20 Nov 2015 16:41:15 -0600 Subject: [PATCH 815/952] Changing from percentages to counts --- lib/heroku/command/plugins.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index e37dda108..7534bb9fb 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -145,8 +145,11 @@ def commands attrs = [:command, :type, :plugin] header = attrs.map{|attr| attr.to_s.capitalize} + count_attrs = [:type, :count] + count_header = count_attrs.map{|attr| attr.to_s.capitalize} + counts = all_cmd.inject(Hash.new(0)) {|h, (_, cmd)| h[cmd[:type]] += 1; h} - type_and_percentage = counts.keys.sort.map{|type| [type, (counts[type].to_f / all_cmd.size).round(2)]} + type_and_percentage = counts.keys.sort.map{|type| {:type => type, :count => counts[type]}} if options[:csv] csv_str = CSV.generate do |csv| @@ -154,17 +157,14 @@ def commands sorted_cmd.each {|cmd| csv << attrs.map{|attr| cmd[attr]}} csv << [] - csv << ['Type', 'Percentage'] - type_and_percentage.each {|type| - csv << type - } + csv << count_header + type_and_percentage.each {|type| csv << count_attrs.map{|attr| type[attr]}} end display(csv_str) else display_table(sorted_cmd, attrs, header) - display("============") - - type_and_percentage.each {|type| display("% #{type[0]}: #{type[1]}")} + display("") + display_table(type_and_percentage, count_attrs, count_header) end end From 66b8e324e49c915e86bf3c92327a7d9c36a15248 Mon Sep 17 00:00:00 2001 From: Josh Sullivan Date: Tue, 1 Dec 2015 13:30:59 -0800 Subject: [PATCH 816/952] spaces: ensure we still request version 3 Platform api requests that don't specify a version will default to v2. We want requests that return information about spaces or org apps to go to v3 instead --- .../{organizations_apps.rb => organizations_apps_v3.rb} | 4 ++-- lib/heroku/api/{spaces.rb => spaces_v3.rb} | 4 ++-- lib/heroku/command/apps.rb | 4 ++-- lib/heroku/command/base.rb | 4 ++-- spec/heroku/command/apps_spec.rb | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) rename lib/heroku/api/{organizations_apps.rb => organizations_apps_v3.rb} (69%) rename lib/heroku/api/{spaces.rb => spaces_v3.rb} (66%) diff --git a/lib/heroku/api/organizations_apps.rb b/lib/heroku/api/organizations_apps_v3.rb similarity index 69% rename from lib/heroku/api/organizations_apps.rb rename to lib/heroku/api/organizations_apps_v3.rb index 26b9fcadf..2b6326577 100644 --- a/lib/heroku/api/organizations_apps.rb +++ b/lib/heroku/api/organizations_apps_v3.rb @@ -1,13 +1,13 @@ module Heroku class API - def post_organizations_app(params={}) + def post_organizations_app_v3(params={}) request( :method => :post, :body => Heroku::Helpers.json_encode(params), :expects => 201, :path => "/organizations/apps", :headers => { - "Accept" => "application/vnd.heroku+json" + "Accept" => "application/vnd.heroku+json; version=3" } ) end diff --git a/lib/heroku/api/spaces.rb b/lib/heroku/api/spaces_v3.rb similarity index 66% rename from lib/heroku/api/spaces.rb rename to lib/heroku/api/spaces_v3.rb index ca9eef18e..0e6caeb7e 100644 --- a/lib/heroku/api/spaces.rb +++ b/lib/heroku/api/spaces_v3.rb @@ -1,12 +1,12 @@ module Heroku class API - def get_space(space_identity) + def get_space_v3(space_identity) request( :method => :get, :expects => [200], :path => "/spaces/#{space_identity}", :headers => { - "Accept" => "application/vnd.heroku+json" + "Accept" => "application/vnd.heroku+json; version=3" } ) end diff --git a/lib/heroku/command/apps.rb b/lib/heroku/command/apps.rb index f26876278..899a4e7a8 100644 --- a/lib/heroku/command/apps.rb +++ b/lib/heroku/command/apps.rb @@ -1,6 +1,6 @@ require "heroku/command/base" require "heroku/command/stack" -require "heroku/api/organizations_apps" +require "heroku/api/organizations_apps_v3" # manage apps (create, destroy) # @@ -124,7 +124,7 @@ def create } info = if options[:space] - api.post_organizations_app(params).body + api.post_organizations_app_v3(params).body elsif org org_api.post_app(params, org).body else diff --git a/lib/heroku/command/base.rb b/lib/heroku/command/base.rb index 1f7c497cd..a11267981 100644 --- a/lib/heroku/command/base.rb +++ b/lib/heroku/command/base.rb @@ -3,7 +3,7 @@ require "heroku/client/rendezvous" require "heroku/client/organizations" require "heroku/command" -require "heroku/api/spaces" +require "heroku/api/spaces_v3" class Heroku::Command::Base include Heroku::Helpers @@ -44,7 +44,7 @@ def org @org ||= if options[:space].is_a?(String) validate_space_xor_org! - api.get_space(options[:space]).body['organization']['name'] + api.get_space_v3(options[:space]).body['organization']['name'] elsif options[:org].is_a?(String) options[:org] elsif options[:personal] || @nil diff --git a/spec/heroku/command/apps_spec.rb b/spec/heroku/command/apps_spec.rb index 2e9608232..795419a12 100644 --- a/spec/heroku/command/apps_spec.rb +++ b/spec/heroku/command/apps_spec.rb @@ -6,7 +6,7 @@ module Heroku::Command before(:each) do stub_core - stub_get_space + stub_get_space_v3 stub_organizations ENV.delete('HEROKU_ORGANIZATION') end @@ -111,7 +111,7 @@ module Heroku::Command context "with a space" do shared_examples "create in a space" do Excon.stub( - :headers => {'Accept' => 'application/vnd.heroku+json'}, + :headers => {'Accept' => 'application/vnd.heroku+json; version=3'}, :method => :post, :path => '/organizations/apps') do { @@ -471,9 +471,9 @@ module Heroku::Command end end - def stub_get_space + def stub_get_space_v3 Excon.stub( - :headers => {'Accept' => 'application/vnd.heroku+json'}, + :headers => {'Accept' => 'application/vnd.heroku+json; version=3'}, :method => :get, :path => '/spaces/test-space') do { From 885fac59023e4b92a2ba2ad0304e0bf9ea1cb903 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 30 Nov 2015 11:15:00 -0600 Subject: [PATCH 817/952] Change exec(args) to exec(shelljoin(string)) * fixes unicode problems on windows --- lib/heroku/jsplugin.rb | 33 +++++++++++++++++++- spec/heroku/jsplugin_spec.rb | 58 ++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 spec/heroku/jsplugin_spec.rb diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 82e6b22db..0233630cc 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -158,7 +158,9 @@ def self.copy_ca_cert def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic - exec self.bin, cmd, *args + + # calling exec with multiple arguments mangles multibyte characters on windows + exec shelljoin([self.bin, cmd, *args]) end def self.spawn(topic, command, args) @@ -166,6 +168,35 @@ def self.spawn(topic, command, args) system self.bin, cmd, *args end + # see https://github.com/ruby/ruby/blob/v2_2_3/lib/shellwords.rb#L149 + def self.shelljoin(array) + array.map { |arg| shellescape(arg) }.join(' ') + end + + # see https://github.com/ruby/ruby/blob/v2_2_3/lib/shellwords.rb#L93 + def self.shellescape(str) + str = str.to_s + + return "''" if str.empty? + + str = str.dup + + begin + # attempt to convert the duplicated string to utf-8 + str.encode!('utf-8') + + # escape while still preserving unicode characters + str.gsub!(/([^\p{L}\p{N}_\-.,:\/@\n])/, "\\\\\\1") + rescue Encoding::UndefinedConversionError + # if it cannot be converted then fall back to normal behavior + str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") + end + + str.gsub!(/\n/, "'\n'") + + return str + end + def self.arch case RbConfig::CONFIG['host_cpu'] when /x86_64/ diff --git a/spec/heroku/jsplugin_spec.rb b/spec/heroku/jsplugin_spec.rb new file mode 100644 index 000000000..8cf5b777d --- /dev/null +++ b/spec/heroku/jsplugin_spec.rb @@ -0,0 +1,58 @@ +require "spec_helper" +require "heroku/jsplugin" + +module Heroku + describe JSPlugin do + context "shellescape" do + it "should to_s the arguments" do + expect(Heroku::JSPlugin.shellescape(3)).to eq("3") + end + + it "should return single quotes for an empty string" do + expect(Heroku::JSPlugin.shellescape('')).to eq("''") + end + + it "escape newlines properly" do + expect(Heroku::JSPlugin.shellescape("\n")).to eq("'\n'") + end + + it "escapes bad shell commands" do + expect(Heroku::JSPlugin.shellescape("`$()|;&><'\"")).to eq("\\`\\$\\(\\)\\|\\;\\&\\>\\<\\'\\\"") + end + + it "passes options through" do + expect(Heroku::JSPlugin.shellescape("-f")).to eq("-f") + end + + it "passes multi byte characters through" do + expect(Heroku::JSPlugin.shellescape("あい")).to eq("あい") + end + + it "passes ascci numbers and letters through without escaping" do + expect(Heroku::JSPlugin.shellescape("Aa0")).to eq("Aa0") + end + + it "passes multi byte characters through" do + expect(Heroku::JSPlugin.shellescape("あい")).to eq("あい") + end + + it "passes multi byte numbers through" do + expect(Heroku::JSPlugin.shellescape("Ⅲ")).to eq("Ⅲ") + end + + it "does not fail on things that cannot be converted to utf-8" do + expected_bad_encoding = "\u0412".force_encoding("ASCII-8BIT") + expected_bad_encoding.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") + + bad_encoding = "\u0412".force_encoding("ASCII-8BIT") + expect(Heroku::JSPlugin.shellescape(bad_encoding)).to eq(expected_bad_encoding) + end + end + + context "shelljoin" do + it "escapes bad shell commands" do + expect(Heroku::JSPlugin.shelljoin(["`$()|;", "&><'\""])).to eq("\\`\\$\\(\\)\\|\\; \\&\\>\\<\\'\\\"") + end + end + end +end From 2cee965ac50e455fdd21972fbe027f5798997801 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 2 Dec 2015 12:18:44 -0600 Subject: [PATCH 818/952] Fixing tests and code for ruby-1.9.3 --- lib/heroku/jsplugin.rb | 2 +- spec/heroku/jsplugin_spec.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 0233630cc..d802c6d0f 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -186,7 +186,7 @@ def self.shellescape(str) str.encode!('utf-8') # escape while still preserving unicode characters - str.gsub!(/([^\p{L}\p{N}_\-.,:\/@\n])/, "\\\\\\1") + str.gsub!(/([^\p{L}\p{N}_\-.,:\/@\n])/u, "\\\\\\1") rescue Encoding::UndefinedConversionError # if it cannot be converted then fall back to normal behavior str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") diff --git a/spec/heroku/jsplugin_spec.rb b/spec/heroku/jsplugin_spec.rb index 8cf5b777d..86c3001ed 100644 --- a/spec/heroku/jsplugin_spec.rb +++ b/spec/heroku/jsplugin_spec.rb @@ -1,3 +1,5 @@ +# encoding: utf-8 + require "spec_helper" require "heroku/jsplugin" From 5424496b031e5ffa3ba0db35832965c1338fcb2e Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 2 Dec 2015 13:28:47 -0600 Subject: [PATCH 819/952] Remove readline dep & unused protected methods --- lib/heroku/command/run.rb | 60 --------------------------------------- 1 file changed, 60 deletions(-) diff --git a/lib/heroku/command/run.rb b/lib/heroku/command/run.rb index c4578673d..83d939420 100644 --- a/lib/heroku/command/run.rb +++ b/lib/heroku/command/run.rb @@ -1,20 +1,3 @@ -begin - require "readline" -rescue LoadError - module Readline - def self.readline(prompt) - print prompt - $stdout.flush - gets - end - - module HISTORY - def self.push(cmd) - # dummy - end - end - end -end require "heroku/command/base" require "heroku/helpers/log_displayer" @@ -140,47 +123,4 @@ def rendezvous_session(rendezvous_url, &on_connect) end end - def console_history_dir - FileUtils.mkdir_p(path = "#{home_directory}/.heroku/console_history") - path - end - - def console_session(app) - heroku.console(app) do |console| - console_history_read(app) - - display "Ruby console for #{app}.#{heroku.host}" - while cmd = Readline.readline('>> ') - unless cmd.nil? || cmd.strip.empty? - console_history_add(app, cmd) - break if cmd.downcase.strip == 'exit' - display console.run(cmd) - end - end - end - end - - def console_history_file(app) - "#{console_history_dir}/#{app}" - end - - def console_history_read(app) - history = File.read(console_history_file(app)).split("\n") - if history.size > 50 - history = history[(history.size - 51),(history.size - 1)] - File.open(console_history_file(app), "w") { |f| f.puts history.join("\n") } - end - history.each { |cmd| Readline::HISTORY.push(cmd) } - rescue Errno::ENOENT - rescue => ex - display "Error reading your console history: #{ex.message}" - if confirm("Would you like to clear it? (y/N):") - FileUtils.rm(console_history_file(app)) rescue nil - end - end - - def console_history_add(app, cmd) - Readline::HISTORY.push(cmd) - File.open(console_history_file(app), "a") { |f| f.puts cmd + "\n" } - end end From 1e3feddf3b163e7efcb7aee89ae0e0ec5513626c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 2 Dec 2015 14:28:11 -0800 Subject: [PATCH 820/952] show extra plugin info such as whether or not the plugin is symlinked --- lib/heroku/command/plugins.rb | 2 +- lib/heroku/jsplugin.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index ec7f84abb..8db0d3528 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -18,7 +18,7 @@ class Plugins < Base def index validate_arguments! - plugins = ::Heroku::JSPlugin.plugins.map { |p| "#{p[:name]}@#{p[:version]}" } + plugins = ::Heroku::JSPlugin.plugins.map { |p| "#{p[:name]}@#{p[:version]} #{p[:extra]}" } plugins.concat(::Heroku::Plugin.list) if plugins.length > 0 diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 82e6b22db..2e5627312 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -40,8 +40,8 @@ def self.load! def self.plugins @plugins ||= `"#{bin}" plugins`.lines.map do |line| - name, version = line.split - { :name => name, :version => version } + name, version, extra = line.split + { :name => name, :version => version, :extra => extra } end end From 653e6540d7db57b61ed2126b9cdd40abeb420366 Mon Sep 17 00:00:00 2001 From: David Brownman Date: Wed, 2 Dec 2015 18:00:33 -0800 Subject: [PATCH 821/952] Be more descriptive about uninstalling Accounts --- lib/heroku/cli.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 01375aa8f..1834ae22f 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -47,7 +47,7 @@ def self.warn_if_using_heroku_accounts if defined?(Heroku::Command::Accounts.account) $stderr.print "Uninstalling deprecated ddollar/heroku-accounts plugin..." Heroku::Plugin.new('heroku-accounts').uninstall - $stderr.puts " done" + $stderr.print "Done. Use https://github.com/heroku/heroku-accounts instead." end end end From 25957fc3ef17e432a8a7738f8589e28e84ba5448 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 2 Dec 2015 10:06:33 -0600 Subject: [PATCH 822/952] Fix Windows cryllic character from ENV issues The underlying problem is that ENV variables with cryllic characters come in as ASCII-8BIT rather than utf-8 * https://bugs.ruby-lang.org/issues/9715 --- lib/heroku/auth.rb | 6 +- lib/heroku/helpers.rb | 27 +++++++- lib/heroku/helpers/env.rb | 15 +++++ lib/heroku/jsplugin.rb | 12 ++-- spec/heroku/auth_spec.rb | 1 + spec/heroku/helpers/env_spec.rb | 45 +++++++++++++ spec/heroku/helpers_spec.rb | 109 ++++++++++++++++++++++++++++++++ spec/heroku/jsplugin_spec.rb | 30 +++++++++ spec/spec_helper.rb | 2 +- 9 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 lib/heroku/helpers/env.rb create mode 100644 spec/heroku/helpers/env_spec.rb diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 330e00372..48d2b3e33 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -2,6 +2,7 @@ require "heroku" require "heroku/client" require "heroku/helpers" +require "heroku/helpers/env" require "netrc" @@ -134,7 +135,10 @@ def legacy_credentials_path end def netrc_path - default = Netrc.default_path + default = File.join(Heroku::Helpers::Env['NETRC'] || home_directory, Netrc.netrc_filename) + # note: the ruby client tries to drop in `pwd` if home does not exist + # but the go client does not, so we do not want the fallback logic + encrypted = default + ".gpg" if File.exists?(encrypted) encrypted diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index ce14ceaf6..33e4cb0ec 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -1,13 +1,36 @@ # encoding: utf-8 +require 'heroku/helpers/env' + module Heroku module Helpers extend self def home_directory - if running_on_windows? && RUBY_VERSION == '1.9.3' - File.expand_path('~') + if running_on_windows? + # This used to be File.expand_path("~"), which should have worked but there was a bug + # when a user has a cryllic character in their username. Their username gets mangled + # by a C code operation that does not respect multibyte characters + # + # see: https://github.com/ruby/ruby/blob/v2_2_3/win32/file.c#L47 + home = Heroku::Helpers::Env["HOME"] + homedrive = Heroku::Helpers::Env["HOMEDRIVE"] + homepath = Heroku::Helpers::Env["HOMEPATH"] + userprofile = Heroku::Helpers::Env["USERPROFILE"] + + home_dir = if home + home + elsif homedrive && homepath + homedrive + homepath + elsif userprofile + userprofile + else + # The expanding `~' error here does not make much sense + # just made it match File.expand_path when no env set + raise ArgumentError.new("couldn't find HOME environment -- expanding `~'") + end + home_dir.gsub(/\\/, '/') else Dir.home end diff --git a/lib/heroku/helpers/env.rb b/lib/heroku/helpers/env.rb new file mode 100644 index 000000000..3596506b2 --- /dev/null +++ b/lib/heroku/helpers/env.rb @@ -0,0 +1,15 @@ +module Heroku + module Helpers + class Env + def self.[](key) + val = ENV[key] + + if val && Heroku::Helpers.running_on_windows? && val.encoding == Encoding::ASCII_8BIT + val = val.dup.force_encoding('utf-8') + end + + val + end + end + end +end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index d802c6d0f..1b95fd6a8 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -1,4 +1,5 @@ require 'rbconfig' +require 'heroku/helpers/env' class Heroku::JSPlugin extend Heroku::Helpers @@ -108,10 +109,13 @@ def self.version end def self.app_dir - if windows? && ENV['LOCALAPPDATA'] - File.join(ENV['LOCALAPPDATA'], 'heroku').encode('windows-1252') - elsif ENV['XDG_DATA_HOME'] - File.join(ENV['XDG_DATA_HOME'], 'heroku') + localappdata = Heroku::Helpers::Env['LOCALAPPDATA'] + xdg_data_home = Heroku::Helpers::Env['XDG_DATA_HOME'] + + if windows? && localappdata + File.join(localappdata, 'heroku') + elsif xdg_data_home + File.join(xdg_data_home, 'heroku') else File.join(Heroku::Helpers.home_directory, '.heroku') end diff --git a/spec/heroku/auth_spec.rb b/spec/heroku/auth_spec.rb index 2098a67fb..bf495fe3e 100644 --- a/spec/heroku/auth_spec.rb +++ b/spec/heroku/auth_spec.rb @@ -23,6 +23,7 @@ module Heroku File.read(path).split("\n").map {|line| "#{line}\n"} end + allow(Heroku::Auth).to receive(:home_directory).and_return(Heroku::Helpers.home_directory) FileUtils.mkdir_p(@cli.netrc_path.split("/")[0..-2].join("/")) File.open(@cli.netrc_path, "w") do |file| diff --git a/spec/heroku/helpers/env_spec.rb b/spec/heroku/helpers/env_spec.rb new file mode 100644 index 000000000..06e6a326b --- /dev/null +++ b/spec/heroku/helpers/env_spec.rb @@ -0,0 +1,45 @@ +# encoding: utf-8 +# +require "spec_helper" +require "heroku/helpers/env" + +module Heroku::Helpers + describe Env do + context "[]" do + + before do + allow(ENV).to receive(:[]).and_return(nil) + allow(Heroku::Helpers).to receive(:running_on_windows?).and_return(true) + end + + after do + allow(ENV).to receive(:[]).and_call_original + allow(Heroku::Helpers).to receive(:running_on_windows?).and_call_original + end + + it "Passes through non ASCII-8BIT strings without re-encoding" do + allow(ENV).to receive(:[]).with('foo').and_return("foo".encode("ISO-8859-1")) + + actual = Heroku::Helpers::Env['foo'] + + expect(actual).to eq("foo") + expect(actual.encoding).to eq(Encoding::ISO_8859_1) + end + + it "Passes through nil without failing" do + allow(ENV).to receive(:[]).with('foo').and_return(nil) + expect(Heroku::Helpers::Env['foo']).to be_nil + end + + it "Attempts to convert ASCII_8BIT" do + bad_encoding = "\u0412".force_encoding("ASCII-8BIT").freeze # verify we work with frozen values + allow(ENV).to receive(:[]).with('foo').and_return(bad_encoding) + + actual = Heroku::Helpers::Env['foo'] + + expect(actual).to eq("\u0412") + expect(actual.encoding).to eq(Encoding::UTF_8) + end + end + end +end diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index 62ae06a5b..ac48c16ee 100644 --- a/spec/heroku/helpers_spec.rb +++ b/spec/heroku/helpers_spec.rb @@ -64,5 +64,114 @@ module Heroku end + context "home_directory" do + before do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_return(true) + + # I would much rather have removed / set ENV variables here, + # but things get manged by the []= operator in windows. + # + # ENV['f'] = "\u0412" ; ENV['f'].encoding == ASCII-8BIT + allow(ENV).to receive(:[]).and_return(nil) + + @tmp_dir = Dir.mktmpdir.encode('utf-8') + @home_dir = File.join(@tmp_dir, "\u0412") + @windows_home_dir = @home_dir.gsub(/\//, "\\") + Dir.mkdir(@home_dir) + end + + after do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_call_original + allow(ENV).to receive(:[]).and_call_original + + Dir.rmdir(@home_dir) + Dir.rmdir(@tmp_dir) + end + + it "should throw ArgumentError when nothing is defined" do + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should handle crillic characters properly in HOME" do + allow(ENV).to receive(:[]).with("HOME").and_return(@windows_home_dir) + allow(ENV).to receive(:[]).with("HOMEPATH").and_return("foo") + allow(ENV).to receive(:[]).with("HOMEDRIVE").and_return("bar") + allow(ENV).to receive(:[]).with("USERPROFILE").and_return("biz") + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should not use HOMEDRIVE when HOMEPATH is not defined" do + allow(ENV).to receive(:[]).with("HOMEDRIVE").and_return(@windows_home_dir[0..1]) + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should handle crillic characters properly in HOMEDRIVE / HOMEPATH" do + allow(ENV).to receive(:[]).with("HOMEDRIVE").and_return(@windows_home_dir[0..1]) + allow(ENV).to receive(:[]).with("HOMEPATH").and_return(@windows_home_dir[2..-1]) + allow(ENV).to receive(:[]).with("USERPROFILE").and_return("biz") + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should handle crillic characters properly in USERPROFILE" do + allow(ENV).to receive(:[]).with("USERPROFILE").and_return(@windows_home_dir) + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + end + + context "home_directory (compatibility)" do + before do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_return(true) + + # I would much rather have removed / set ENV variables here, + # but things get manged by the []= operator in windows. + # + # ENV['f'] = "\u0412" ; ENV['f'].encoding == ASCII-8BIT + @home = ENV.delete('HOME') + @home_drive = ENV.delete('HOMEDRIVE') + @home_path = ENV.delete('HOMEPATH') + @user_profile = ENV.delete('USERPROFILE') + + @home_dir = Heroku::Helpers.home_directory + @windows_home_dir = @home_dir.gsub(/\//, "\\") + end + + after do + allow(Heroku::Helpers).to receive(:running_on_windows?).and_call_original + + ENV['HOME'] = @home + ENV['HOMEDRIVE'] = @home_drive + ENV['HOMEPATH'] = @home_path + ENV['USERPROFILE'] = @user_profile + end + + it "should throw ArgumentError when nothing is defined" do + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should use HOME" do + ENV["HOME"] = @windows_home_dir + ENV["HOMEPATH"] = "foo" + ENV["HOMEDRIVE"] = "bar" + ENV["USERPROFILE"] = "biz" + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should not use HOMEDRIVE when HOMEPATH is not defined" do + ENV["HOMEDRIVE"] = @windows_home_dir[0..1] + expect{ Heroku::Helpers.orig_home_directory }.to raise_error(ArgumentError) + end + + it "should use HOMEDRIVE / HOMEPATH" do + ENV["HOMEDRIVE"] = @windows_home_dir[0..1] + ENV["HOMEPATH"] = @windows_home_dir[2..-1] + ENV["USERPROFILE"] = "biz" + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + + it "should use USERPROFILE" do + ENV["USERPROFILE"] = @windows_home_dir + expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) + end + end end end diff --git a/spec/heroku/jsplugin_spec.rb b/spec/heroku/jsplugin_spec.rb index 86c3001ed..961f47e3d 100644 --- a/spec/heroku/jsplugin_spec.rb +++ b/spec/heroku/jsplugin_spec.rb @@ -56,5 +56,35 @@ module Heroku expect(Heroku::JSPlugin.shelljoin(["`$()|;", "&><'\""])).to eq("\\`\\$\\(\\)\\|\\; \\&\\>\\<\\'\\\"") end end + + context "app_dir" do + before do + allow(Heroku::JSPlugin).to receive(:windows?).and_return(true) + allow(ENV).to receive(:[]).and_return(nil) + end + + it "should use LOCALAPPDATA only in windows" do + allow(ENV).to receive(:[]).with("LOCALAPPDATA").and_return("foo") + allow(ENV).to receive(:[]).with("XDG_DATA_HOME").and_return("bar") + expect(Heroku::JSPlugin.app_dir).to eq(File.join("foo", "heroku")) + + allow(Heroku::JSPlugin).to receive(:windows?).and_return(false) + expect(Heroku::JSPlugin.app_dir).to eq(File.join("bar", "heroku")) + end + + it "should not use XDG_DATA_HOME if defined" do + allow(ENV).to receive(:[]).with("XDG_DATA_HOME").and_return("bar") + expect(Heroku::JSPlugin.app_dir).to eq(File.join("bar", "heroku")) + end + + it "should default to home directory" do + expect(Heroku::JSPlugin.app_dir).to eq(File.join(Heroku::Helpers.home_directory, ".heroku")) + end + + after do + allow(Heroku::JSPlugin).to receive(:windows?).and_call_original + allow(ENV).to receive(:[]).and_call_original + end + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d8a421a89..fbb73c2e2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -220,7 +220,7 @@ def bash(cmd) require "heroku/helpers" module Heroku::Helpers @home_directory = Dir.mktmpdir - undef_method :home_directory + alias_method :orig_home_directory, :home_directory def home_directory @home_directory end From 1de8d0b799cd65cb4610fb391aaceca51c48d230 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 3 Dec 2015 16:08:26 -0600 Subject: [PATCH 823/952] v3.42.23 --- CHANGELOG | 16 ++++++++++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 710727377..4f4311184 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,19 @@ +3.42.23 2015-12-03 +================== +Update RELEASE-FULL.md OSX notes +switch to osslsigncode to fix windows installs +upgrade ruby to 2.1.7 and git to 2.6.3 on windows +small copy tweak +clear up unknown database error +Remove rendezvous spec that fails when not a tty +Add plugins:command to track progress +spaces: ensure we still request version 3 +Change exec(args) to exec(shelljoin(string)) +Remove readline dep & unused protected methods +show extra plugin info +Be more descriptive about uninstalling Accounts +Fix Windows cryllic character from ENV issues + 3.42.22 2015-11-18 ================== prefix update messaging diff --git a/Gemfile.lock b/Gemfile.lock index 4da883d1d..717b6568c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.22) + heroku (3.42.23) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f781642ac..d64e4cbc9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.22" + VERSION = "3.42.23" end From 08363737ec0e2f7ab87bd43414e541659bd7a136 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 3 Dec 2015 16:24:42 -0600 Subject: [PATCH 824/952] Fix Windows exe release instruction typo --- RELEASE-FULL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-FULL.md b/RELEASE-FULL.md index 72fef7cc6..7eea7508b 100644 --- a/RELEASE-FULL.md +++ b/RELEASE-FULL.md @@ -45,7 +45,7 @@ rm XQuartz-2.7.6.dmg * Initialize wine: `bundle exec rake exe:init-wine` To build for testing: `bundle exec rake exe:build`. Outputs to `./dist/heroku-toolbelt-X.Y.Z.exe`. -To release: `bundle exec rake pkg:release`. +To release: `bundle exec rake exe:release`. ## Main Release From a49788c7f56e2f3910d21866ed98eb6e74b07695 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 3 Dec 2015 15:33:38 -0800 Subject: [PATCH 825/952] add v4 channel support allows updating to beta channel in v4 --- lib/heroku/command/update.rb | 3 ++- lib/heroku/jsplugin.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/heroku/command/update.rb b/lib/heroku/command/update.rb index eaf7e74b6..19b8c2e71 100644 --- a/lib/heroku/command/update.rb +++ b/lib/heroku/command/update.rb @@ -15,8 +15,9 @@ class Heroku::Command::Update < Heroku::Command::Base # Updating... done, v1.2.3 updated to v2.3.4 # def index + channel = shift_argument validate_arguments! - Heroku::JSPlugin.update + Heroku::JSPlugin.update(channel) update_from_url(false) end diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index d802c6d0f..789d612b4 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -99,8 +99,8 @@ def self.uninstall(name) system "\"#{bin}\" plugins:uninstall #{name}" end - def self.update - system "\"#{bin}\" update" + def self.update(channel='') + system "\"#{bin}\" update #{channel}" end def self.version From 5da1866b4d974ec9ffe27ce52c63429f02400006 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 4 Dec 2015 09:55:39 -0600 Subject: [PATCH 826/952] Revert exec to shellescape while I fix windows bug --- lib/heroku/jsplugin.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 0ec74b928..55f81d7ec 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -162,9 +162,7 @@ def self.copy_ca_cert def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic - - # calling exec with multiple arguments mangles multibyte characters on windows - exec shelljoin([self.bin, cmd, *args]) + exec self.bin, cmd, *args end def self.spawn(topic, command, args) From 69157c1f2fe0c9c0cb94a48c4ce6cc9a217e143e Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 4 Dec 2015 10:08:50 -0600 Subject: [PATCH 827/952] v3.42.24 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4f4311184..0ba91984f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.24 2015-12-04 +================== +Revert exec to shellescape while I fix windows bug + 3.42.23 2015-12-03 ================== Update RELEASE-FULL.md OSX notes diff --git a/Gemfile.lock b/Gemfile.lock index 717b6568c..6b1fe4c40 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.23) + heroku (3.42.24) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d64e4cbc9..2ddeac97d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.23" + VERSION = "3.42.24" end From b2934cbdf1f8ab69c7c3354e2dffcaaffa70b5f8 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 7 Dec 2015 14:40:54 -0800 Subject: [PATCH 828/952] Use system & exit on windows when non-ascii args --- lib/heroku/jsplugin.rb | 38 ++++++-------------------- spec/heroku/jsplugin_spec.rb | 52 ------------------------------------ 2 files changed, 8 insertions(+), 82 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 55f81d7ec..106e153a9 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -162,7 +162,14 @@ def self.copy_ca_cert def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic - exec self.bin, cmd, *args + bin = self.bin + + if windows? && [bin, cmd, *args].any? {|arg| ! arg.ascii_only?} + system bin, cmd, *args + exit $?.exitstatus + else + exec bin, cmd, *args + end end def self.spawn(topic, command, args) @@ -170,35 +177,6 @@ def self.spawn(topic, command, args) system self.bin, cmd, *args end - # see https://github.com/ruby/ruby/blob/v2_2_3/lib/shellwords.rb#L149 - def self.shelljoin(array) - array.map { |arg| shellescape(arg) }.join(' ') - end - - # see https://github.com/ruby/ruby/blob/v2_2_3/lib/shellwords.rb#L93 - def self.shellescape(str) - str = str.to_s - - return "''" if str.empty? - - str = str.dup - - begin - # attempt to convert the duplicated string to utf-8 - str.encode!('utf-8') - - # escape while still preserving unicode characters - str.gsub!(/([^\p{L}\p{N}_\-.,:\/@\n])/u, "\\\\\\1") - rescue Encoding::UndefinedConversionError - # if it cannot be converted then fall back to normal behavior - str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") - end - - str.gsub!(/\n/, "'\n'") - - return str - end - def self.arch case RbConfig::CONFIG['host_cpu'] when /x86_64/ diff --git a/spec/heroku/jsplugin_spec.rb b/spec/heroku/jsplugin_spec.rb index 961f47e3d..6e99bb605 100644 --- a/spec/heroku/jsplugin_spec.rb +++ b/spec/heroku/jsplugin_spec.rb @@ -5,58 +5,6 @@ module Heroku describe JSPlugin do - context "shellescape" do - it "should to_s the arguments" do - expect(Heroku::JSPlugin.shellescape(3)).to eq("3") - end - - it "should return single quotes for an empty string" do - expect(Heroku::JSPlugin.shellescape('')).to eq("''") - end - - it "escape newlines properly" do - expect(Heroku::JSPlugin.shellescape("\n")).to eq("'\n'") - end - - it "escapes bad shell commands" do - expect(Heroku::JSPlugin.shellescape("`$()|;&><'\"")).to eq("\\`\\$\\(\\)\\|\\;\\&\\>\\<\\'\\\"") - end - - it "passes options through" do - expect(Heroku::JSPlugin.shellescape("-f")).to eq("-f") - end - - it "passes multi byte characters through" do - expect(Heroku::JSPlugin.shellescape("あい")).to eq("あい") - end - - it "passes ascci numbers and letters through without escaping" do - expect(Heroku::JSPlugin.shellescape("Aa0")).to eq("Aa0") - end - - it "passes multi byte characters through" do - expect(Heroku::JSPlugin.shellescape("あい")).to eq("あい") - end - - it "passes multi byte numbers through" do - expect(Heroku::JSPlugin.shellescape("Ⅲ")).to eq("Ⅲ") - end - - it "does not fail on things that cannot be converted to utf-8" do - expected_bad_encoding = "\u0412".force_encoding("ASCII-8BIT") - expected_bad_encoding.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1") - - bad_encoding = "\u0412".force_encoding("ASCII-8BIT") - expect(Heroku::JSPlugin.shellescape(bad_encoding)).to eq(expected_bad_encoding) - end - end - - context "shelljoin" do - it "escapes bad shell commands" do - expect(Heroku::JSPlugin.shelljoin(["`$()|;", "&><'\""])).to eq("\\`\\$\\(\\)\\|\\; \\&\\>\\<\\'\\\"") - end - end - context "app_dir" do before do allow(Heroku::JSPlugin).to receive(:windows?).and_return(true) From d7c7290d89f96f82d5b6a0c211c1a7b359778cee Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Tue, 8 Dec 2015 13:41:27 -0600 Subject: [PATCH 829/952] v3.42.25 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 0ba91984f..26de78352 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.25 2015-12-08 +================== +Use system & exit on windows when non-ascii args + 3.42.24 2015-12-04 ================== Revert exec to shellescape while I fix windows bug diff --git a/Gemfile.lock b/Gemfile.lock index 6b1fe4c40..fa40df46a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.24) + heroku (3.42.25) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2ddeac97d..7455373be 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.24" + VERSION = "3.42.25" end From aa0482a30b1bf3a420353eb8183acf79d6e63438 Mon Sep 17 00:00:00 2001 From: Atul Bhosale Date: Sun, 3 Jan 2016 18:19:04 +0530 Subject: [PATCH 830/952] Update copyright notice to 2016 [ci skip] --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index ffaa7a305..db2f0eea3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © Heroku 2008 - 2014 +Copyright © Heroku 2008 - 2016 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From 3c55b4846be0f209d93ba66454002aaeff249deb Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 7 Jan 2016 08:54:44 -0600 Subject: [PATCH 831/952] Fix bug with international characters & createdb --- lib/heroku/helpers/pg_dump_restore.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/heroku/helpers/pg_dump_restore.rb b/lib/heroku/helpers/pg_dump_restore.rb index b47e15f5f..68e5ce371 100644 --- a/lib/heroku/helpers/pg_dump_restore.rb +++ b/lib/heroku/helpers/pg_dump_restore.rb @@ -40,7 +40,13 @@ def create_local_db dbname = @target.path[1..-1] cdb_output = `createdb #{dbname} 2>&1` if $?.exitstatus != 0 - if cdb_output =~ /already exists/ + already_exists = false + begin + already_exists = cdb_output =~ /already exists/ + rescue ArgumentError + # invalid byte sequence in UTF-8 on windows + end + if already_exists command.error(cdb_output + "\nPlease drop the local database (`dropdb #{dbname}`) and try again.") else command.error(cdb_output + "\nUnable to create new local database. Ensure your local Postgres is working and try again.") From f4c8a0bf8d7b7eaa0e08aeba4f21c6e0035ea4c0 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 7 Jan 2016 10:49:46 -0600 Subject: [PATCH 832/952] v3.42.26 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 26de78352..7fdaa8deb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.42.26 2016-01-07 +================== +Fix bug with international characters & createdb +Update copyright notice to 2016 + 3.42.25 2015-12-08 ================== Use system & exit on windows when non-ascii args diff --git a/Gemfile.lock b/Gemfile.lock index fa40df46a..da308d2a0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.25) + heroku (3.42.26) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 7455373be..b42068656 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.25" + VERSION = "3.42.26" end From e64036dc0890dfce7113c0328c1e8d4715036a51 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 7 Jan 2016 14:52:27 -0600 Subject: [PATCH 833/952] v3.42.27 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7fdaa8deb..19cbe59e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.27 2016-01-07 +================== +Bump version for release + 3.42.26 2016-01-07 ================== Fix bug with international characters & createdb diff --git a/Gemfile.lock b/Gemfile.lock index da308d2a0..3d1aad68a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.26) + heroku (3.42.27) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index b42068656..3da3c1f1d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.26" + VERSION = "3.42.27" end From c4376f96d13e0960eb7ce5368734006e4a6b1b01 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Mon, 3 Aug 2015 13:14:53 -0700 Subject: [PATCH 834/952] added cli analytics --- lib/heroku/analytics.rb | 64 +++++++++++++++++++++++++++++++++++++++++ lib/heroku/cli.rb | 4 +++ lib/heroku/config.rb | 27 +++++++++++++++++ spec/spec_helper.rb | 2 ++ 4 files changed, 97 insertions(+) create mode 100644 lib/heroku/analytics.rb create mode 100644 lib/heroku/config.rb diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb new file mode 100644 index 000000000..dafa66ea9 --- /dev/null +++ b/lib/heroku/analytics.rb @@ -0,0 +1,64 @@ +class Heroku::Analytics + extend Heroku::Helpers + + def self.record(command) + return if skip_analytics + File.open(path, 'a') do |f| + f.write("#{command}|#{Time.now.to_i}\n") + end + rescue + end + + def self.submit + return if skip_analytics + lines = File.read(path).split("\n") + return if lines.count < 10 # only submit if we have 10 entries to send + fork do + commands = lines.map do |line| + line = line.split('|') + {command: line[0], timestamp: line[1].to_i} + end + payload = { + user: user, + commands: commands + } + Excon.post('https://heroku-cli-analytics.herokuapp.com/record', body: JSON.dump(payload)) + File.truncate(path, 0) + end + rescue + end + + private + + def self.skip_analytics + return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) + skip = Heroku::Config[:skip_analytics] + if skip == nil + # user has not specified whether or not they want to submit usage information + # prompt them to ask, but if they wait more than 20 seconds just assume they + # want to skip analytics + require 'timeout' + stderr_print "Would you like to submit Heroku CLI usage information to better improve the CLI user experience?\n[y/N] " + input = begin + Timeout::timeout(20) do + ask.downcase + end + rescue + stderr_puts 'n' + end + Heroku::Config[:skip_analytics] = !['y', 'yes'].include?(input) + Heroku::Config.save! + end + + skip + end + + def self.path + File.join(Heroku::Helpers.home_directory, ".heroku", "analytics") + end + + def self.user + credentials = Heroku::Auth.read_credentials + credentials[0] if credentials + end +end diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 1834ae22f..1542eeee2 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -9,6 +9,8 @@ require 'heroku' require 'heroku/jsplugin' +require 'heroku/config' +require 'heroku/analytics' require 'heroku/rollbar' require 'json' @@ -21,6 +23,7 @@ def self.start(*args) $stdout.sync = true if $stdout.isatty Heroku::Updater.warn_if_updating command = args.shift.strip rescue "help" + Heroku::Analytics.record(command) Heroku::JSPlugin.setup Heroku::JSPlugin.try_takeover(command, args) require 'heroku/command' @@ -28,6 +31,7 @@ def self.start(*args) Heroku::Command.load warn_if_using_heroku_accounts Heroku::Command.run(command, args) + Heroku::Analytics.submit Heroku::Updater.autoupdate rescue Errno::EPIPE => e error(e.message) diff --git a/lib/heroku/config.rb b/lib/heroku/config.rb new file mode 100644 index 000000000..7504f155e --- /dev/null +++ b/lib/heroku/config.rb @@ -0,0 +1,27 @@ +class Heroku::Config + extend Heroku::Helpers + + def self.[](key) + config[key.to_s] + end + + def self.[]=(key, value) + config[key.to_s] = value + end + + def self.save! + File.open(path, 'w') do |f| + f.puts(JSON.pretty_generate(config)) + end + end + + private + + def self.config + @config ||= JSON.parse(File.read(path)) rescue {} + end + + def self.path + File.join(Heroku::Helpers.home_directory, ".heroku", "config.json") + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index fbb73c2e2..3ff832d65 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,6 +22,8 @@ require "webmock/rspec" require "shellwords" +ENV['HEROKU_SKIP_ANALYTICS'] = '1' + include WebMock::API WebMock::HttpLibAdapters::ExconAdapter.disable! From ea9a2d07c0d40cf8484c4bce5116ea43608e5043 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 15 Dec 2015 15:37:09 -0800 Subject: [PATCH 835/952] use json for analytics --- lib/heroku/analytics.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index dafa66ea9..06ab39057 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -3,24 +3,20 @@ class Heroku::Analytics def self.record(command) return if skip_analytics - File.open(path, 'a') do |f| - f.write("#{command}|#{Time.now.to_i}\n") - end + commands = json_decode(File.read(path)) || [] rescue [] + commands << {command: command, timestamp: Time.now.to_i, version: Heroku.user_agent} + File.open(path, 'w') { |f| f.write(json_encode(commands)) } rescue end def self.submit return if skip_analytics - lines = File.read(path).split("\n") - return if lines.count < 10 # only submit if we have 10 entries to send + commands = json_decode(File.read(path)) + return if commands.count < 10 # only submit if we have 10 entries to send fork do - commands = lines.map do |line| - line = line.split('|') - {command: line[0], timestamp: line[1].to_i} - end payload = { user: user, - commands: commands + commands: commands, } Excon.post('https://heroku-cli-analytics.herokuapp.com/record', body: JSON.dump(payload)) File.truncate(path, 0) @@ -54,7 +50,7 @@ def self.skip_analytics end def self.path - File.join(Heroku::Helpers.home_directory, ".heroku", "analytics") + File.join(Heroku::Helpers.home_directory, ".heroku", "analytics.json") end def self.user From e9e94d8878b1f7afce2c2d77837a4c631474edf5 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 13:19:13 -0800 Subject: [PATCH 836/952] set analytics host to heroku.com url --- lib/heroku/analytics.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 06ab39057..e8584ff7c 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -18,7 +18,7 @@ def self.submit user: user, commands: commands, } - Excon.post('https://heroku-cli-analytics.herokuapp.com/record', body: JSON.dump(payload)) + Excon.post('https://cli-analytics.heroku.com/record', body: JSON.dump(payload)) File.truncate(path, 0) end rescue From 4688b36fc8ebd8dcad94687143d6c8fa3df40b8c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 13:19:24 -0800 Subject: [PATCH 837/952] skip prompt for analytics if not a tty --- lib/heroku/analytics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index e8584ff7c..b4016aecd 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -30,6 +30,7 @@ def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) skip = Heroku::Config[:skip_analytics] if skip == nil + return false unless $stdin.isatty # user has not specified whether or not they want to submit usage information # prompt them to ask, but if they wait more than 20 seconds just assume they # want to skip analytics From 868b7e2396a2d702bf0e3d394013307ec50ecf97 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 13:19:41 -0800 Subject: [PATCH 838/952] ensure analytics is skipped the first time if needed --- lib/heroku/analytics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index b4016aecd..a44c245b9 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -45,6 +45,7 @@ def self.skip_analytics end Heroku::Config[:skip_analytics] = !['y', 'yes'].include?(input) Heroku::Config.save! + return Heroku::Config[:skip_analytics] end skip From 447c27a2fe29b4b99b428bf4ac27607c5d8dfe1c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 13:46:19 -0800 Subject: [PATCH 839/952] added attributes --- lib/heroku/analytics.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index a44c245b9..412605d78 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -4,7 +4,7 @@ class Heroku::Analytics def self.record(command) return if skip_analytics commands = json_decode(File.read(path)) || [] rescue [] - commands << {command: command, timestamp: Time.now.to_i, version: Heroku.user_agent} + commands << {command: command, timestamp: Time.now.to_i, version: Heroku::VERSION, platform: RUBY_PLATFORM, language: "ruby/#{RUBY_VERSION}"} File.open(path, 'w') { |f| f.write(json_encode(commands)) } rescue end From 6a2c6d171b4d39ec1b2e9c29287141c41a9a6064 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 13:48:40 -0800 Subject: [PATCH 840/952] deprecate heroku-spaces plugin --- lib/heroku/plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index ad1a7214d..91643eb43 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -25,6 +25,7 @@ class ErrorUpdatingSymlinkPlugin < StandardError; end heroku-push heroku-releases heroku-shared-postgresql + heroku-spaces heroku-sql-console heroku-status heroku-stop From ae8f9bdf4fbea707435abbdffe3960ee24b936ae Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 14:14:00 -0800 Subject: [PATCH 841/952] use /verysilent to install git on windows --- resources/exe/heroku.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 6686e9eff..62da887c8 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -43,7 +43,7 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen [Run] Filename: "{tmp}\rubyinstaller-2.1.7.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.1.7"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" -Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/silent /nocancel /noicons"; \ +Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/verysilent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" [UninstallDelete] From 7a072c85c12c630ff5f878f622f8953aa4a44b23 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 14:15:14 -0800 Subject: [PATCH 842/952] v3.42.28 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 19cbe59e2..3dbb560bd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.42.28 2016-01-13 +================== +Added analytics +Install git with /verysilent on windows +Deprecate heroku-spaces ruby plugin + 3.42.27 2016-01-07 ================== Bump version for release diff --git a/Gemfile.lock b/Gemfile.lock index 3d1aad68a..f796eb287 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.27) + heroku (3.42.28) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 3da3c1f1d..6d7bd8571 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.27" + VERSION = "3.42.28" end From 82992921fd726a2e17921f1cea317e5c7330a154 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 18:36:16 -0800 Subject: [PATCH 843/952] skip analytics if not a tty --- lib/heroku/analytics.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 412605d78..93789dbf8 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -30,7 +30,7 @@ def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) skip = Heroku::Config[:skip_analytics] if skip == nil - return false unless $stdin.isatty + return true unless $stdin.isatty # user has not specified whether or not they want to submit usage information # prompt them to ask, but if they wait more than 20 seconds just assume they # want to skip analytics From da78b55d2a1efdade58ed6d809e004923feb4526 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 13 Jan 2016 18:37:12 -0800 Subject: [PATCH 844/952] v3.42.29 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3dbb560bd..c54b6ad3c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.29 2016-01-13 +================== +Fixed analytics to not report when not a tty + 3.42.28 2016-01-13 ================== Added analytics diff --git a/Gemfile.lock b/Gemfile.lock index f796eb287..033ff28a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.28) + heroku (3.42.29) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 6d7bd8571..313aad12d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.28" + VERSION = "3.42.29" end From c94f44e617a9f9c1d79db325cf5cc2bced251105 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 14 Jan 2016 13:06:53 -0800 Subject: [PATCH 845/952] delete unupdatable versions of v4 will fix this: https://rollbar.com/heroku-cli/heroku-cli/items/584/ --- lib/heroku/jsplugin.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 106e153a9..7011e8ecc 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -226,9 +226,10 @@ def self.find_command(s) commands.find { |c| c[:command] == s } end - # check if release is one that isn't able to update on windows + # check if release is one that isn't updateable def self.check_if_old File.delete(bin) if windows? && setup? && version.start_with?("heroku-cli/4.24") + File.delete(bin) if setup? && version.start_with?("heroku-cli/4.27.5-") rescue => e Rollbar.error(e) rescue From 4e5699a76f189accf3d0cc6c16b8803988d03067 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 19 Jan 2016 10:24:18 -0800 Subject: [PATCH 846/952] better error for unsupported osx ppc machines Fixes #1878 --- lib/heroku/jsplugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 106e153a9..47fcef256 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -191,6 +191,7 @@ def self.arch def self.os case RbConfig::CONFIG['host_os'] when /darwin|mac os/ + raise "#{arch} is not supported" unless arch == "amd64" "darwin" when /linux/ "linux" From 517f74b08167ec72b147ff9907db6ab26e79ee77 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 19 Jan 2016 14:49:22 -0800 Subject: [PATCH 847/952] remove duplicate command names in suggester because we actually have multiple commands that are the same (ruby + node for example). We need to uniquely filter this list. To repro: `heroku remote help` --- lib/heroku/command.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 503823d23..fc9247db1 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -201,7 +201,7 @@ def self.prepare_run(cmd, args=[]) else error([ "`#{cmd}` is not a heroku command.", - suggestion(cmd, commands.keys + command_aliases.keys), + suggestion(cmd, (commands.keys + command_aliases.keys).uniq), "See `heroku help` for a list of available commands." ].compact.join("\n")) end From 22fd313665a8b51665e56d2b450aa60d468b8a16 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 19 Jan 2016 17:10:15 -0800 Subject: [PATCH 848/952] fix old netrc gems installs --- lib/heroku/auth.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 48d2b3e33..5c2add8ae 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -135,7 +135,11 @@ def legacy_credentials_path end def netrc_path - default = File.join(Heroku::Helpers::Env['NETRC'] || home_directory, Netrc.netrc_filename) + default = begin + File.join(Heroku::Helpers::Env['NETRC'] || home_directory, Netrc.netrc_filename) + rescue NoMethodError # happens if old netrc gem is installed + Netrc.default_path + end # note: the ruby client tries to drop in `pwd` if home does not exist # but the go client does not, so we do not want the fallback logic From 29f6b9667ff88ee5f7a72243a1bbd8eea2fca445 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Tue, 19 Jan 2016 17:13:02 -0800 Subject: [PATCH 849/952] v3.42.30 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c54b6ad3c..ed4522365 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.42.30 2016-01-18 +================== +Fix for old netrc gem installs +Remove duplicate commands from suggester +Show better error for OSX PPC machines +Allow updating to dev channel for v4 + 3.42.29 2016-01-13 ================== Fixed analytics to not report when not a tty diff --git a/Gemfile.lock b/Gemfile.lock index 033ff28a9..d98dab980 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.29) + heroku (3.42.30) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 313aad12d..2f7a5de8d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.29" + VERSION = "3.42.30" end From 2fec333b58e26612501ec5139c915dbd93b0a0ad Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 21 Jan 2016 09:00:34 -0600 Subject: [PATCH 850/952] Submit analytics in the foreground on windows --- lib/heroku/analytics.rb | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 93789dbf8..a4d65a718 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -13,19 +13,28 @@ def self.submit return if skip_analytics commands = json_decode(File.read(path)) return if commands.count < 10 # only submit if we have 10 entries to send - fork do - payload = { - user: user, - commands: commands, - } - Excon.post('https://cli-analytics.heroku.com/record', body: JSON.dump(payload)) - File.truncate(path, 0) + begin + fork do + submit_analytics(user, commands, path) + end + rescue NotImplementedError + # cannot fork on windows + submit_analytics(user, commands, path) end rescue end private + def self.submit_analytics(user, commands, path) + payload = { + user: user, + commands: commands, + } + Excon.post('https://cli-analytics.heroku.com/record', body: JSON.dump(payload)) + File.truncate(path, 0) + end + def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) skip = Heroku::Config[:skip_analytics] From e33910b72b32fd93b48aa9d8549476bc3c174aab Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 21 Jan 2016 10:11:41 -0600 Subject: [PATCH 851/952] v3.42.31 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ed4522365..f8874977d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.31 2016-01-21 +================== +Submit analytics in the foreground on windows + 3.42.30 2016-01-18 ================== Fix for old netrc gem installs diff --git a/Gemfile.lock b/Gemfile.lock index d98dab980..b37186319 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.30) + heroku (3.42.31) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2f7a5de8d..ada74f0a5 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.30" + VERSION = "3.42.31" end From 00ae5e4644f1f163548bed6010ef97bfe1962d48 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 27 Jan 2016 12:30:30 -0800 Subject: [PATCH 852/952] skip analytics on codeship codeship runs as a tty so it tries to prompt for the analytics --- lib/heroku/analytics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 93789dbf8..febd9b5ab 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -28,6 +28,7 @@ def self.submit def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) + return true if ENV['CODESHIP'] == 'true' skip = Heroku::Config[:skip_analytics] if skip == nil return true unless $stdin.isatty From 42d61d61e0731f7bbbfae5889bafe4e22fcc57b1 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 27 Jan 2016 14:34:23 -0800 Subject: [PATCH 853/952] skip analytics for now --- lib/heroku/analytics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index c54273876..469c41ef4 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -36,6 +36,7 @@ def self.submit_analytics(user, commands, path) end def self.skip_analytics + return true # skip analytics for now return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' skip = Heroku::Config[:skip_analytics] From 9cbb4543920728357f694b667faf3d15318d2031 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Wed, 27 Jan 2016 14:35:35 -0800 Subject: [PATCH 854/952] v3.42.32 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f8874977d..ecabdc753 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.32 2016-01-27 +================== +Skip analytics for now + 3.42.31 2016-01-21 ================== Submit analytics in the foreground on windows diff --git a/Gemfile.lock b/Gemfile.lock index b37186319..54bfd8b97 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.31) + heroku (3.42.32) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index ada74f0a5..428de2788 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.31" + VERSION = "3.42.32" end From b4067674f22e256771d55db51f4ebc553ed50e64 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 28 Jan 2016 13:21:14 -0800 Subject: [PATCH 855/952] deprecate ruby sudo --- lib/heroku/plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index 91643eb43..0d7cb320a 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -29,6 +29,7 @@ class ErrorUpdatingSymlinkPlugin < StandardError; end heroku-sql-console heroku-status heroku-stop + heroku-sudo heroku-suggest heroku-symbol heroku-two-factor From e23208162b408bbbccdfedaca68f4e4f9b00173c Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 28 Jan 2016 13:33:12 -0800 Subject: [PATCH 856/952] v3.42.33 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index ecabdc753..9f320c246 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.33 2016-01-28 +================== +Deprecated ruby sudo + 3.42.32 2016-01-27 ================== Skip analytics for now diff --git a/Gemfile.lock b/Gemfile.lock index 54bfd8b97..c557e26e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.32) + heroku (3.42.33) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 428de2788..938cfb93d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.32" + VERSION = "3.42.33" end From 8c2c5e3c31d99613b055185dacac0e161fd91a0c Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 8 Feb 2016 11:32:43 -0600 Subject: [PATCH 857/952] Encode error as utf-8 before joining * Fixes https://github.com/heroku/heroku/issues/1402 --- lib/heroku/helpers.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 33e4cb0ec..92af75fac 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -433,9 +433,8 @@ def format_error(error, message='Heroku client internal error.', rollbar_id=nil) command = ARGV.map do |arg| if arg.include?(' ') arg = %{"#{arg}"} - else - arg end + arg.encode('utf-8') end.join(' ') formatted_error << " Command: heroku #{command}" require 'heroku/auth' From 88ab9723a9926752abcb317204ae828c9e705763 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 8 Feb 2016 11:39:42 -0600 Subject: [PATCH 858/952] v3.42.34 --- CHANGELOG | 4 ++++ Gemfile.lock | 5 +---- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9f320c246..308347037 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.34 2016-02-08 +================== +Encode error as utf-8 before joining + 3.42.33 2016-01-28 ================== Deprecated ruby sudo diff --git a/Gemfile.lock b/Gemfile.lock index c557e26e0..f641b087c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.33) + heroku (3.42.34) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -101,6 +101,3 @@ DEPENDENCIES rr rspec webmock - -BUNDLED WITH - 1.10.6 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 938cfb93d..d6b12079f 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.33" + VERSION = "3.42.34" end From 939361f0b46fcb1416083be1c320ac72d7a8c5bb Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 10 Feb 2016 09:17:27 -0600 Subject: [PATCH 859/952] Scrub URI::InvalidURIError message for rollbar --- lib/heroku/rollbar.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/heroku/rollbar.rb b/lib/heroku/rollbar.rb index ef26c9e44..c48e1a160 100644 --- a/lib/heroku/rollbar.rb +++ b/lib/heroku/rollbar.rb @@ -1,3 +1,5 @@ +require 'uri' + module Rollbar extend Heroku::Helpers @@ -49,11 +51,16 @@ def self.base_payload end def self.trace_from_exception(e) + message = if e.is_a?(URI::InvalidURIError) + "[scrubbed]" + else + e.message + end { :frames => frames_from_exception(e), :exception => { :class => e.class.to_s, - :message => e.message + :message => message } } end From 6373d2ed7d4c2bf38321a508043ca51f9424e82d Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 10 Feb 2016 09:25:53 -0600 Subject: [PATCH 860/952] v3.42.35 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 308347037..212289ecb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.35 2016-02-10 +================== +Scrub URI::InvalidURIError message for rollbar + 3.42.34 2016-02-08 ================== Encode error as utf-8 before joining diff --git a/Gemfile.lock b/Gemfile.lock index f641b087c..374177681 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.34) + heroku (3.42.35) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d6b12079f..c7b1a3a50 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.34" + VERSION = "3.42.35" end From 2d6726b095e0bed33e2584fddfe0a47897515ced Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 11 Feb 2016 14:11:03 -0600 Subject: [PATCH 861/952] Call org_api.lock_app when --locked flag is passed --- lib/heroku/client/organizations.rb | 4 ++-- lib/heroku/command/sharing.rb | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/heroku/client/organizations.rb b/lib/heroku/client/organizations.rb index 314b624d1..b6d8eed44 100644 --- a/lib/heroku/client/organizations.rb +++ b/lib/heroku/client/organizations.rb @@ -111,12 +111,12 @@ def post_app(params, org) ) end - def transfer_app(to_org, app, locked) + def transfer_app(to_org, app) api.request( :expects => 200, :method => :put, :path => "/v1/app/#{app}", - :body => Heroku::Helpers.json_encode( { "owner" => to_org, "locked" => locked || 'false' } ), + :body => Heroku::Helpers.json_encode( { "owner" => to_org } ), :headers => {"Content-Type" => "application/json"} ) end diff --git a/lib/heroku/command/sharing.rb b/lib/heroku/command/sharing.rb index c975b999d..6f6d1951e 100644 --- a/lib/heroku/command/sharing.rb +++ b/lib/heroku/command/sharing.rb @@ -103,11 +103,11 @@ def transfer action("Transferring #{app} to #{target}") do if org || !target.include?('@') - locked = options[:locked] - - org_api.transfer_app(target, app, locked) - display("App is locked. Organization members must be invited to access.") if locked - + org_api.transfer_app(target, app) + if options[:locked] + org_api.lock_app(app) + display("App is locked. Organization members must be invited to access.") + end else api.put_app(app, "transfer_owner" => target) end From 2fca11f0bb080a9ce0d7f5727cf731d2ed54c423 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 11 Feb 2016 14:55:33 -0600 Subject: [PATCH 862/952] v3.42.36 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 212289ecb..3e49b9aee 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.36 2016-02-11 +================== +Call org_api.lock_app when --locked flag is passed + 3.42.35 2016-02-10 ================== Scrub URI::InvalidURIError message for rollbar diff --git a/Gemfile.lock b/Gemfile.lock index 374177681..e802b09f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.35) + heroku (3.42.36) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index c7b1a3a50..0770a7733 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.35" + VERSION = "3.42.36" end From dc49888341aee131bf360c5be782865317d939f4 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 15 Feb 2016 11:19:15 -0600 Subject: [PATCH 863/952] Adding 2.3.0 to .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index ee73af1a5..8238f6f8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ rvm: - 2.0.0 - 2.1.5 - 2.2.0 + - 2.3.0 sudo: false From 9aa92e7177ea7ba813480a6e986dad1a4b1b1ee8 Mon Sep 17 00:00:00 2001 From: Tim Carey-Smith Date: Fri, 19 Feb 2016 10:35:31 -0800 Subject: [PATCH 864/952] Clarify version for pg:upgrade Explain that pg:copy is an alternative --- lib/heroku/command/pg.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 0b92cbbce..9341c1cbc 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -463,7 +463,9 @@ def maintenance # pg:upgrade REPLICA # - # unfollow a database and upgrade it to the latest PostgreSQL version + # unfollow a database and upgrade it to the latest stable PostgreSQL version + # + # To upgrade to another PostgreSQL version, use pg:copy instead # def upgrade requires_preauth From 5fcbc7da6f8d4ae47a410fa7d9d8515e3f22c2c3 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 22 Feb 2016 10:42:42 -0600 Subject: [PATCH 865/952] Add fix for Gem::Specification method_missing --- bin/heroku | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/bin/heroku b/bin/heroku index 75ec5149f..5facf9f0c 100755 --- a/bin/heroku +++ b/bin/heroku @@ -8,6 +8,13 @@ bin_file = Pathname.new(__FILE__).realpath # add self to libpath $:.unshift File.expand_path("../../lib", bin_file) +# Fixes https://github.com/rubygems/rubygems/issues/1420 +require 'rubygems/specification' + +class Gem::Specification + def this; self; end +end + require "heroku/updater" Heroku::Updater.disable("`heroku update` is only available from Heroku Toolbelt.\nDownload and install from https://toolbelt.heroku.com") From 00bff8bc82db214074b7444bd3aaeebf5950a242 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 22 Feb 2016 15:55:29 -0600 Subject: [PATCH 866/952] v3.42.37 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3e49b9aee..7209c21f3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.37 2016-02-22 +================== +Add fix for Gem::Specification method_missing + 3.42.36 2016-02-11 ================== Call org_api.lock_app when --locked flag is passed diff --git a/Gemfile.lock b/Gemfile.lock index e802b09f4..3abcf7085 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.36) + heroku (3.42.37) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0770a7733..9ab133443 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.36" + VERSION = "3.42.37" end From 51b50c9ea9ff3c9994d7ab64853f659f9065d769 Mon Sep 17 00:00:00 2001 From: Raul Barroso Date: Tue, 23 Feb 2016 21:20:34 -0800 Subject: [PATCH 867/952] Removes heroku:sharing --- lib/heroku/command/sharing.rb | 117 ---------------------------- spec/heroku/command/sharing_spec.rb | 59 -------------- 2 files changed, 176 deletions(-) delete mode 100644 lib/heroku/command/sharing.rb delete mode 100644 spec/heroku/command/sharing_spec.rb diff --git a/lib/heroku/command/sharing.rb b/lib/heroku/command/sharing.rb deleted file mode 100644 index 6f6d1951e..000000000 --- a/lib/heroku/command/sharing.rb +++ /dev/null @@ -1,117 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # manage collaborators on an app - # - class Sharing < Base - - # sharing - # - # list collaborators on an app - # - #Example: - # - # $ heroku sharing - # === example Collaborators - # collaborator@example.com collaborator - # email@example.com owner - # - def index - validate_arguments! - - # this is never empty, as it always includes the owner - collaborators = api.get_collaborators(app).body - collaborators = collaborators.delete_if { |collaborator| org? collaborator["email"] } - - styled_header("#{app} Access List") - styled_array(collaborators.map {|collaborator| [collaborator["email"], collaborator.fetch("role", "collaborator")] }) - end - - # sharing:add EMAIL - # - # add a collaborator to an app - # - #Example: - # - # $ heroku sharing:add collaborator@example.com - # Adding collaborator@example.com to example collaborators... done - # - def add - unless email = shift_argument - error("Usage: heroku sharing:add EMAIL\nMust specify EMAIL to add sharing.") - end - validate_arguments! - org_from_app! - - action("Adding #{email} to #{app} as collaborator") do - if org && org_api.get_members(org).body.map { |m| m['email'] }.include?(email) - org_api.post_collaborator(org, app, email) - else - api.post_collaborator(app, email) - end - end - end - - # sharing:remove EMAIL - # - # remove a collaborator from an app - # - #Example: - # - # $ heroku sharing:remove collaborator@example.com - # Removing collaborator@example.com to example collaborators... done - # - def remove - unless email = shift_argument - error("Usage: heroku sharing:remove EMAIL\nMust specify EMAIL to remove sharing.") - end - validate_arguments! - org_from_app! - - action("Removing #{email} from #{app} collaborators") do - if org && org_api.get_members(org).body.map { |m| m['email'] }.include?(email) - org_api.delete_collaborator(org, app, email) - else - api.delete_collaborator(app, email) - end - end - end - - # sharing:transfer TARGET - # - # transfers an app to another user or an organization. - # TARGET is the email of another user or the name of the - # organization to transfer to. - # - #Example: - # - # $ heroku sharing:transfer collaborator@example.com - # Transferring example to collaborator@example.com... done - # - # $ heroku sharing:transfer acme-widgets - # Transferring example to acme-widgets... done - # - # -l, --locked # lock the app upon transfer - # - def transfer - unless target = shift_argument - error("Usage: heroku sharing:transfer EMAIL\nMust specify EMAIL to transfer an app.") - end - validate_arguments! - org_from_app! - - action("Transferring #{app} to #{target}") do - if org || !target.include?('@') - org_api.transfer_app(target, app) - if options[:locked] - org_api.lock_app(app) - display("App is locked. Organization members must be invited to access.") - end - else - api.put_app(app, "transfer_owner" => target) - end - end - end - end -end diff --git a/spec/heroku/command/sharing_spec.rb b/spec/heroku/command/sharing_spec.rb deleted file mode 100644 index 080506f82..000000000 --- a/spec/heroku/command/sharing_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -require "spec_helper" -require "heroku/command/sharing" - -module Heroku::Command - describe Sharing do - - before(:each) do - stub_core - api.post_app("name" => "example") - end - - after(:each) do - api.delete_app("example") - end - - context("list") do - - it "lists collaborators" do - api.post_collaborator("example", "collaborator@example.com") - stderr, stdout = execute("sharing") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -=== example Access List -collaborator@example.com collaborator -email@example.com collaborator - -STDOUT - end - - end - - it "adds collaborators with default access to view only" do - stderr, stdout = execute("sharing:add collaborator@example.com") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Adding collaborator@example.com to example as collaborator... done -STDOUT - end - - it "removes collaborators" do - api.post_collaborator("example", "collaborator@example.com") - stderr, stdout = execute("sharing:remove collaborator@example.com") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Removing collaborator@example.com from example collaborators... done -STDOUT - end - - it "transfers ownership" do - api.post_collaborator("example", "collaborator@example.com") - stderr, stdout = execute("sharing:transfer collaborator@example.com") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -Transferring example to collaborator@example.com... done -STDOUT - end - end - -end From 0d3acad07bdbfe680a5c714452215594febada59 Mon Sep 17 00:00:00 2001 From: dickeyxxx Date: Thu, 25 Feb 2016 10:24:47 -0800 Subject: [PATCH 868/952] v3.42.38 --- CHANGELOG | 4 ++++ Gemfile.lock | 5 ++++- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 7209c21f3..a89ea986c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.38 2016-02-25 +================== +Remove sharing commands (available as access now) + 3.42.37 2016-02-22 ================== Add fix for Gem::Specification method_missing diff --git a/Gemfile.lock b/Gemfile.lock index 3abcf7085..620627017 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.37) + heroku (3.42.38) heroku-api (= 0.3.23) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -101,3 +101,6 @@ DEPENDENCIES rr rspec webmock + +BUNDLED WITH + 1.10.6 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9ab133443..122d48223 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.37" + VERSION = "3.42.38" end From 904bdfcb9daf3b5c595135b2f53d0fa978ba2143 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 2 Mar 2016 13:29:21 -0600 Subject: [PATCH 869/952] Bump heroku-api dependency to 0.4.2 --- Gemfile.lock | 10 +++++----- heroku.gemspec | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 620627017..7ba62f6ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: heroku (3.42.38) - heroku-api (= 0.3.23) + heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) net-ssh (= 2.9.2) @@ -31,10 +31,10 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.45.4) + excon (0.47.0) fakefs (0.6.7) - heroku-api (0.3.23) - excon (~> 0.44) + heroku-api (0.4.2) + excon (~> 0.45) multi_json (~> 1.8) json (1.8.2) launchy (2.4.3) @@ -103,4 +103,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.10.6 + 1.11.2 diff --git a/heroku.gemspec b/heroku.gemspec index 3aff6c37d..a974ad5a0 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |gem| gem.files = %x{ git ls-files }.split("\n").select { |d| d =~ %r{^(LICENSE|README|bin/|data/|ext/|lib/|spec/|test/)} } - gem.add_dependency "heroku-api", "0.3.23" + gem.add_dependency "heroku-api", "0.4.2" gem.add_dependency "launchy", "2.4.3" gem.add_dependency "netrc", "0.10.3" gem.add_dependency "rest-client", "1.6.8" From 1941d224d00d8bfb2daa92b008d84b8cb68b3f85 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 2 Mar 2016 13:30:53 -0600 Subject: [PATCH 870/952] v3.42.39 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a89ea986c..e326f64e1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.39 2016-03-02 +================== +Bump heroku-api dependency to 0.4.2 + 3.42.38 2016-02-25 ================== Remove sharing commands (available as access now) diff --git a/Gemfile.lock b/Gemfile.lock index 7ba62f6ad..3eff4b372 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.38) + heroku (3.42.39) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 122d48223..8577ac57e 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.38" + VERSION = "3.42.39" end From c6d5538ed588d0bd3c8fa5c51436b11893f6b669 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 7 Mar 2016 13:36:15 -0600 Subject: [PATCH 871/952] Add ruby 2.2 and 2.3 to package dependencies --- resources/deb/heroku/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/deb/heroku/control b/resources/deb/heroku/control index c7f9a96df..bbe07e573 100644 --- a/resources/deb/heroku/control +++ b/resources/deb/heroku/control @@ -3,6 +3,6 @@ Version: <%= version %> Section: main Priority: standard Architecture: all -Depends: ruby2.1|ruby2.0|libopenssl-ruby1.9.1, ruby2.1|ruby2.0|libreadline-ruby1.9.1, ruby2.1|ruby2.0|ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 +Depends: ruby2.3|ruby2.2|ruby2.1|ruby2.0|libopenssl-ruby1.9.1, ruby2.3|ruby2.2|ruby2.1|ruby2.0|libreadline-ruby1.9.1, ruby2.3|ruby2.2|ruby2.1|ruby2.0|ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 Maintainer: Heroku Description: Client library and CLI to deploy apps on Heroku. From 9b752c0b1c39cbb106833389bf8881f7e11c4f84 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 7 Mar 2016 15:49:45 -0600 Subject: [PATCH 872/952] v3.42.40 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e326f64e1..8923177e2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.40 2016-03-07 +================== +Add ruby 2.2 and 2.3 to package dependencies + 3.42.39 2016-03-02 ================== Bump heroku-api dependency to 0.4.2 diff --git a/Gemfile.lock b/Gemfile.lock index 3eff4b372..7031c5650 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.39) + heroku (3.42.40) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 8577ac57e..02709c112 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.39" + VERSION = "3.42.40" end From e3c34bc719bdc01af5ebaccaa776c89e1170ee4f Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 14 Mar 2016 09:43:56 -0500 Subject: [PATCH 873/952] Treat empty HEROKU_API_KEY the same as not set --- lib/heroku/auth.rb | 4 ++-- lib/heroku/command.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/auth.rb b/lib/heroku/auth.rb index 5c2add8ae..503fdbd90 100644 --- a/lib/heroku/auth.rb +++ b/lib/heroku/auth.rb @@ -167,7 +167,7 @@ def netrc # :nodoc: end def read_credentials - if ENV['HEROKU_API_KEY'] + if ENV['HEROKU_API_KEY'] && ENV['HEROKU_API_KEY'] != '' ['', ENV['HEROKU_API_KEY']] else # convert legacy credentials to netrc @@ -284,7 +284,7 @@ def ask_for_and_save_credentials rescue Heroku::API::Errors::NotFound, Heroku::API::Errors::Unauthorized => e delete_credentials display "Authentication failed." - warn "WARNING: HEROKU_API_KEY is set to an invalid key." if ENV['HEROKU_API_KEY'] + warn "WARNING: HEROKU_API_KEY is set to an invalid key." if ENV['HEROKU_API_KEY'] && ENV['HEROKU_API_KEY'] != '' retry if retry_login? exit 1 rescue => e diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index fc9247db1..5b8f85afc 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -266,7 +266,7 @@ def self.run(cmd, arguments=[]) end def self.handle_auth_error(e) - if ENV['HEROKU_API_KEY'] + if ENV['HEROKU_API_KEY'] && ENV['HEROKU_API_KEY'] != '' puts "Authentication failure with HEROKU_API_KEY" exit 1 elsif wrong_two_factor_code?(e) From 8a3381796a7f322abe37e6f45e535334ef6c5e8c Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 14 Mar 2016 14:30:19 -0500 Subject: [PATCH 874/952] v3.42.41 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 8923177e2..676c9113d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.41 2016-03-14 +================== +Treat empty HEROKU_API_KEY the same as not set + 3.42.40 2016-03-07 ================== Add ruby 2.2 and 2.3 to package dependencies diff --git a/Gemfile.lock b/Gemfile.lock index 7031c5650..81eeeb342 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.40) + heroku (3.42.41) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 02709c112..f5f065f35 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.40" + VERSION = "3.42.41" end From 8fdcb47e50db5fab8d09feb7fcf4b9e6791f0d14 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Mon, 14 Mar 2016 15:44:24 -0500 Subject: [PATCH 875/952] v3.42.42 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 676c9113d..1902a1f0a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.42 2016-03-14 +================== +Bump version after fix in build server process + 3.42.41 2016-03-14 ================== Treat empty HEROKU_API_KEY the same as not set diff --git a/Gemfile.lock b/Gemfile.lock index 81eeeb342..eb69bad24 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.41) + heroku (3.42.42) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f5f065f35..4a004a290 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.41" + VERSION = "3.42.42" end From 7e25e709555de7661317ff159e2230820b36633b Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 18 Mar 2016 16:52:37 -0500 Subject: [PATCH 876/952] Fix invalid byte sequence in format_with_bang --- lib/heroku/helpers.rb | 2 +- spec/heroku/helpers_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 92af75fac..30e4b4efa 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -300,7 +300,7 @@ def status(message) def format_with_bang(message) return '' if message.to_s.strip == "" - " ! " + message.split("\n").join("\n ! ") + " ! " + message.encode('utf-8', invalid: :replace, undef: :replace).split("\n").join("\n ! ") end def output_with_bang(message="", new_line=true) diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index ac48c16ee..95cea7d9d 100644 --- a/spec/heroku/helpers_spec.rb +++ b/spec/heroku/helpers_spec.rb @@ -173,5 +173,13 @@ module Heroku expect(Heroku::Helpers.orig_home_directory).to eq(@home_dir) end end + + context "format_with_bang" do + it "should not fail with bad utf characters" do + message = "hello joel\255".force_encoding('UTF-8') + expect(" ! hello joel�").to eq(format_with_bang(message)) + end + end + end end From 173e0b6b3fb6c3d60691464027eed28424e4feea Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 18 Mar 2016 17:05:36 -0500 Subject: [PATCH 877/952] v3.42.43 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 1902a1f0a..4c1e9e917 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.43 2016-03-18 +================== +Fix invalid byte sequence in format_with_bang + 3.42.42 2016-03-14 ================== Bump version after fix in build server process diff --git a/Gemfile.lock b/Gemfile.lock index eb69bad24..65900699b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.42) + heroku (3.42.43) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4a004a290..49e6bbf78 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.42" + VERSION = "3.42.43" end From 493cbbeead9355a3db2e2941b3921504c1dcc32e Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 18 Mar 2016 18:27:04 -0500 Subject: [PATCH 878/952] Fix encode & unicode character for 1.9.3 --- lib/heroku/helpers.rb | 2 +- spec/heroku/helpers_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 30e4b4efa..0890d5aa1 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -300,7 +300,7 @@ def status(message) def format_with_bang(message) return '' if message.to_s.strip == "" - " ! " + message.encode('utf-8', invalid: :replace, undef: :replace).split("\n").join("\n ! ") + " ! " + message.encode('utf-8', 'binary', invalid: :replace, undef: :replace).split("\n").join("\n ! ") end def output_with_bang(message="", new_line=true) diff --git a/spec/heroku/helpers_spec.rb b/spec/heroku/helpers_spec.rb index 95cea7d9d..b4f70481f 100644 --- a/spec/heroku/helpers_spec.rb +++ b/spec/heroku/helpers_spec.rb @@ -177,7 +177,7 @@ module Heroku context "format_with_bang" do it "should not fail with bad utf characters" do message = "hello joel\255".force_encoding('UTF-8') - expect(" ! hello joel�").to eq(format_with_bang(message)) + expect(" ! hello joel\u{FFFD}").to eq(format_with_bang(message)) end end From 556508b88543bc56c6192fbcea1ae40a30e291d3 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 18 Mar 2016 18:32:12 -0500 Subject: [PATCH 879/952] v3.42.44 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4c1e9e917..24f5bad88 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.44 2016-03-18 +================== +Fix encode & unicode character for 1.9.3 + 3.42.43 2016-03-18 ================== Fix invalid byte sequence in format_with_bang diff --git a/Gemfile.lock b/Gemfile.lock index 65900699b..b1de8707d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.43) + heroku (3.42.44) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 49e6bbf78..6a88da559 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.43" + VERSION = "3.42.44" end From 7458a29ec8a8bf90fb296281912208de007a953e Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Tue, 29 Mar 2016 15:44:40 -0500 Subject: [PATCH 880/952] Only execute git installer if no registry key --- resources/exe/heroku.iss | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 62da887c8..17ec7d821 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -24,13 +24,12 @@ Name: custom; Description: "Custom Installation"; flags: iscustom [Components] Name: "toolbelt"; Description: "Heroku Toolbelt"; Types: "client custom" Name: "toolbelt/client"; Description: "Heroku Client"; Types: "client custom"; Flags: fixed -Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: "not IsProgramInstalled('git-2.6.3.exe')" -Name: "toolbelt/git"; Description: "Git and SSH"; Check: "IsProgramInstalled('git-2.6.3.exe')" +Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: IsGitNotInstalled() [Files] Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" Source: "installers\rubyinstaller-2.1.7.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" -Source: "installers\git-2.6.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/git" +Source: "installers\git-2.6.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/git"; Check: IsGitNotInstalled() [Registry] Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "HerokuPath"; \ @@ -38,13 +37,13 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ ValueData: "{olddata};{app}\bin"; Check: NeedsAddPath(ExpandConstant('{app}\bin')) Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "Path"; \ - ValueData: "{olddata};{pf}\git\cmd"; Check: NeedsAddPath(ExpandConstant('{pf}\git\cmd')) + ValueData: "{olddata};{pf}\git\cmd"; Check: IsGitNotInstalled() and NeedsAddPath(ExpandConstant('{pf}\git\cmd')) [Run] Filename: "{tmp}\rubyinstaller-2.1.7.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.1.7"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/verysilent /nocancel /noicons"; \ - Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git" + Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git"; Check: IsGitNotInstalled() [UninstallDelete] Type: filesandordirs; Name: "{localappdata}\heroku" @@ -68,9 +67,7 @@ begin Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; end; -function IsProgramInstalled(Name: string): boolean; -var - ResultCode: integer; +function IsGitNotInstalled(): boolean; begin - Result := Exec(Name, 'version', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + Result := not RegKeyExists(HKLM, 'Software\GitForWindows'); end; From 28250455d1be4c27a66c1067c9dd51974dad0a57 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 30 Mar 2016 10:05:04 -0500 Subject: [PATCH 881/952] Add in extra registry key for old copies of Git --- resources/exe/heroku.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index 17ec7d821..e411a4540 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -69,5 +69,5 @@ end; function IsGitNotInstalled(): boolean; begin - Result := not RegKeyExists(HKLM, 'Software\GitForWindows'); + Result := not (RegKeyExists(HKLM, 'Software\GitForWindows') or RegKeyExists(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1') or RegKeyExists(HKCU, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1')) end; From be2722861303283196a89308e9b518ed8d0db6c1 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 30 Mar 2016 10:15:24 -0500 Subject: [PATCH 882/952] Upgrading git installer from 2.6.3 to 2.8.0 --- resources/exe/heroku.iss | 4 ++-- tasks/exe.rake | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/exe/heroku.iss b/resources/exe/heroku.iss index e411a4540..4896cbd65 100644 --- a/resources/exe/heroku.iss +++ b/resources/exe/heroku.iss @@ -29,7 +29,7 @@ Name: "toolbelt/git"; Description: "Git and SSH"; Types: "client custom"; Check: [Files] Source: "heroku\*.*"; DestDir: "{app}"; Flags: recursesubdirs; Components: "toolbelt/client" Source: "installers\rubyinstaller-2.1.7.exe"; DestDir: "{tmp}"; Components: "toolbelt/client" -Source: "installers\git-2.6.3.exe"; DestDir: "{tmp}"; Components: "toolbelt/git"; Check: IsGitNotInstalled() +Source: "installers\git-2.8.0.exe"; DestDir: "{tmp}"; Components: "toolbelt/git"; Check: IsGitNotInstalled() [Registry] Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; ValueType: "expandsz"; ValueName: "HerokuPath"; \ @@ -42,7 +42,7 @@ Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environmen [Run] Filename: "{tmp}\rubyinstaller-2.1.7.exe"; Parameters: "/verysilent /noreboot /nocancel /noicons /dir=""{app}/ruby-2.1.7"""; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Ruby"; Components: "toolbelt/client" -Filename: "{tmp}\git-2.6.3.exe"; Parameters: "/verysilent /nocancel /noicons"; \ +Filename: "{tmp}\git-2.8.0.exe"; Parameters: "/verysilent /nocancel /noicons"; \ Flags: shellexec waituntilterminated; StatusMsg: "Installing Git"; Components: "toolbelt/git"; Check: IsGitNotInstalled() [UninstallDelete] diff --git a/tasks/exe.rake b/tasks/exe.rake index d11cd2b0c..8a7213126 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -63,7 +63,7 @@ file dist("heroku-toolbelt-#{version}.exe") => "zip:build" do |exe_task| # gather the ruby and git installers, downlading from s3 mkdir "#{installer_path}/installers" cd "#{installer_path}/installers" do - ["rubyinstaller-2.1.7.exe", "git-2.6.3.exe"].each { |i| cp cache_file_from_bucket(i), i } + ["rubyinstaller-2.1.7.exe", "git-2.8.0.exe"].each { |i| cp cache_file_from_bucket(i), i } end # add windows helper executables to the heroku cli From 7e8f69d46b6d04320e015920995f1c8cf35bbe3f Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 30 Mar 2016 10:51:45 -0500 Subject: [PATCH 883/952] Add code to fail the rake task if download fails --- tasks/exe.rake | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tasks/exe.rake b/tasks/exe.rake index 8a7213126..a66bdf41c 100644 --- a/tasks/exe.rake +++ b/tasks/exe.rake @@ -44,7 +44,12 @@ end def cache_file_from_bucket(filename) FileUtils.mkdir_p $cache_path file_cache_path = File.join($cache_path, filename) - system "curl -# https://heroku-toolbelt.s3.amazonaws.com/#{filename} -o '#{file_cache_path}'" unless File.exists? file_cache_path + system "curl -f -# https://heroku-toolbelt.s3.amazonaws.com/#{filename} -o '#{file_cache_path}'" unless File.exists? file_cache_path + unless $?.exitstatus === 0 + puts("Could not download #{filename}, please check permissions manually") + File.delete(file_cache_path) if File.exists?(file_cache_path) + exit(1) + end file_cache_path end From 491f06ba03734b2606bbcf50c7fae323f0ecf39a Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 30 Mar 2016 14:16:37 -0500 Subject: [PATCH 884/952] v3.42.45 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 24f5bad88..61ebd7c8d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.42.45 2016-03-30 +================== +Only execute git installer if no registry key +Upgrading git installer from 2.6.3 to 2.8.0 + 3.42.44 2016-03-18 ================== Fix encode & unicode character for 1.9.3 diff --git a/Gemfile.lock b/Gemfile.lock index b1de8707d..27c01110f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.44) + heroku (3.42.45) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 6a88da559..0c9a86dcb 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.44" + VERSION = "3.42.45" end From a654a85be0cacb1ab57d63c9d733c56dbfd41029 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 5 Apr 2016 14:46:58 -0700 Subject: [PATCH 885/952] re-enabled analytics this just shows a single message once notifying users them the analytics will be enabled. This fixes problems with users using it in scripts and is generally less annoying than it was before. --- lib/heroku/analytics.rb | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 469c41ef4..b0f368e91 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -36,30 +36,16 @@ def self.submit_analytics(user, commands, path) end def self.skip_analytics - return true # skip analytics for now return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' - skip = Heroku::Config[:skip_analytics] - if skip == nil - return true unless $stdin.isatty - # user has not specified whether or not they want to submit usage information - # prompt them to ask, but if they wait more than 20 seconds just assume they - # want to skip analytics - require 'timeout' - stderr_print "Would you like to submit Heroku CLI usage information to better improve the CLI user experience?\n[y/N] " - input = begin - Timeout::timeout(20) do - ask.downcase - end - rescue - stderr_puts 'n' - end - Heroku::Config[:skip_analytics] = !['y', 'yes'].include?(input) + + if Heroku::Config[:skip_analytics] == nil + stderr_puts "Heroku CLI submits usage information back to Heroku. If you would like to disable this, set `skip_analytics: true` in #{Heroku::Config.path}" + Heroku::Config[:skip_analytics] = false Heroku::Config.save! - return Heroku::Config[:skip_analytics] end - skip + Heroku::Config[:skip_analytics] end def self.path From 94a3917fd8f7d6cd820df10a3367c2c153faa6ee Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 6 Apr 2016 13:12:52 -0700 Subject: [PATCH 886/952] v3.42.46 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 61ebd7c8d..59d4cafa2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.46 2016-04-06 +================== +Enabled analytics + 3.42.45 2016-03-30 ================== Only execute git installer if no registry key diff --git a/Gemfile.lock b/Gemfile.lock index 27c01110f..8554688b4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.45) + heroku (3.42.46) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 0c9a86dcb..2a203dd19 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.45" + VERSION = "3.42.46" end From 78da2c2b09b4db5e8962f9e34c30d56c6f1ed4c8 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 6 Apr 2016 18:22:43 -0700 Subject: [PATCH 887/952] skip analytics if no user --- lib/heroku/analytics.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index b0f368e91..5e65736a7 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -1,3 +1,5 @@ +require 'heroku/auth' + class Heroku::Analytics extend Heroku::Helpers @@ -36,6 +38,7 @@ def self.submit_analytics(user, commands, path) end def self.skip_analytics + return true unless user return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' @@ -54,6 +57,7 @@ def self.path def self.user credentials = Heroku::Auth.read_credentials - credentials[0] if credentials + return unless credentials + credentials[0] == '' ? nil : credentials[0] end end From 2eda9f8e86cd27a896fe021618a5130560b80d84 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 6 Apr 2016 18:23:16 -0700 Subject: [PATCH 888/952] v3.42.47 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 59d4cafa2..6e4831d2b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.47 2016-04-06 +================== +Skip analytics if no user + 3.42.46 2016-04-06 ================== Enabled analytics diff --git a/Gemfile.lock b/Gemfile.lock index 8554688b4..893393fec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.46) + heroku (3.42.47) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2a203dd19..ac80a67d2 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.46" + VERSION = "3.42.47" end From 6e91e64c0b6045ab3dd7810ded05ed4f06971677 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 8 Apr 2016 15:28:14 -0500 Subject: [PATCH 889/952] Change exit code for ubuntu update to zero --- lib/heroku/updater.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index 3c039876a..f2b7f6acd 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -70,7 +70,8 @@ def self.disable(message=nil) def self.check_disabled! if disable - Heroku::Helpers.error(disable) + $stderr.puts(format_with_bang(disable)) + exit(0) end end From 6bb203dc9868db7d63f1de880ac2b0ce19540fdc Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 8 Apr 2016 15:44:07 -0500 Subject: [PATCH 890/952] v3.42.48 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 6e4831d2b..aa986d448 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.48 2016-04-08 +================== +Change exit code for ubuntu update to zero + 3.42.47 2016-04-06 ================== Skip analytics if no user diff --git a/Gemfile.lock b/Gemfile.lock index 893393fec..9d1330c79 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.47) + heroku (3.42.48) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index ac80a67d2..e6a158a32 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.47" + VERSION = "3.42.48" end From 34e0b2b1bf3f9a778c542c7f4eec408cead18aca Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 07:11:25 -0700 Subject: [PATCH 891/952] take out install message about v4 it has been out long enough to serve its purpose Fixes https://github.com/heroku/heroku-cli/issues/209 --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index a9a240d9d..cc531f49b 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -145,7 +145,7 @@ def self.setup File.delete bin raise 'SHA mismatch for heroku-cli' end - $stderr.puts " done\nFor more information on Toolbelt v4: https://github.com/heroku/heroku-cli" + $stderr.puts " done" version end From 2114da67ceaeac89d481c1d825f528c044ce2e4b Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 12:46:52 -0700 Subject: [PATCH 892/952] added more analytics data --- lib/heroku/analytics.rb | 11 ++++++++++- lib/heroku/cli.rb | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 5e65736a7..1b90592d6 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -6,7 +6,16 @@ class Heroku::Analytics def self.record(command) return if skip_analytics commands = json_decode(File.read(path)) || [] rescue [] - commands << {command: command, timestamp: Time.now.to_i, version: Heroku::VERSION, platform: RUBY_PLATFORM, language: "ruby/#{RUBY_VERSION}"} + commands << { + command: command, + timestamp: Time.now.to_i, + cli_version: Heroku.user_agent, + version: Heroku::VERSION, + os: Heroku::JSPlugin.os, + arch: Heroku::JSPlugin.arch, + language: "ruby/#{RUBY_VERSION}", + valid: !!Heroku::Command.parse(command), + } File.open(path, 'w') { |f| f.write(json_encode(commands)) } rescue end diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 1542eeee2..34387f25a 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -23,12 +23,13 @@ def self.start(*args) $stdout.sync = true if $stdout.isatty Heroku::Updater.warn_if_updating command = args.shift.strip rescue "help" - Heroku::Analytics.record(command) + Heroku::Analytics.skip_analytics # just sets the config for the analytics Heroku::JSPlugin.setup Heroku::JSPlugin.try_takeover(command, args) require 'heroku/command' Heroku::Git.check_git_version Heroku::Command.load + Heroku::Analytics.record(command) warn_if_using_heroku_accounts Heroku::Command.run(command, args) Heroku::Analytics.submit From 0939e041a50844aab5189225f793327b318adfe8 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 12:48:20 -0700 Subject: [PATCH 893/952] v3.42.49 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index aa986d448..10473e0fc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.42.49 2016-04-09 +================== +Cleaned up output when installing v4 CLI +Analytics update + 3.42.48 2016-04-08 ================== Change exit code for ubuntu update to zero diff --git a/Gemfile.lock b/Gemfile.lock index 9d1330c79..8cb832f30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.48) + heroku (3.42.49) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e6a158a32..d19901426 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.48" + VERSION = "3.42.49" end From 0496eb688134cbcdc2123d48c5275b6caa6ff473 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 13:33:07 -0700 Subject: [PATCH 894/952] fix travis --- lib/heroku/analytics.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 1b90592d6..c738bf1f7 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -47,9 +47,9 @@ def self.submit_analytics(user, commands, path) end def self.skip_analytics - return true unless user return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' + return true unless user if Heroku::Config[:skip_analytics] == nil stderr_puts "Heroku CLI submits usage information back to Heroku. If you would like to disable this, set `skip_analytics: true` in #{Heroku::Config.path}" From 11c75dc6bb5273d6da4855f4f055b741b0e2cd0d Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 15:48:13 -0700 Subject: [PATCH 895/952] fix some js plugins from showing up in analytics --- lib/heroku/analytics.rb | 4 +++- lib/heroku/jsplugin.rb | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index c738bf1f7..aa16ce59a 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -6,6 +6,8 @@ class Heroku::Analytics def self.record(command) return if skip_analytics commands = json_decode(File.read(path)) || [] rescue [] + c = Heroku::Command.parse(command) + return if c && c[:js] commands << { command: command, timestamp: Time.now.to_i, @@ -14,7 +16,7 @@ def self.record(command) os: Heroku::JSPlugin.os, arch: Heroku::JSPlugin.arch, language: "ruby/#{RUBY_VERSION}", - valid: !!Heroku::Command.parse(command), + valid: !!c, } File.open(path, 'w') { |f| f.write(json_encode(commands)) } rescue diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index cc531f49b..a18f8999c 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -78,6 +78,7 @@ def initialize(args, opts) :help => help, :hidden => command['hidden'], :default => command['default'], + :js => true, } end end From 800cb99945d4eb4bbd9507341dbab8ed6fce7e91 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 17:21:55 -0700 Subject: [PATCH 896/952] fix bug reading config on first load --- lib/heroku/config.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/config.rb b/lib/heroku/config.rb index 7504f155e..888971234 100644 --- a/lib/heroku/config.rb +++ b/lib/heroku/config.rb @@ -18,6 +18,7 @@ def self.save! private def self.config + FileUtils.mkdir_p File.dirname(path) @config ||= JSON.parse(File.read(path)) rescue {} end From 977d0493d3dba5e25600182ec558d8c2eb2ab5d8 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 9 Apr 2016 17:22:39 -0700 Subject: [PATCH 897/952] v3.42.50 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 10473e0fc..dbecfc4db 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.42.50 2016-04-09 +================== +Fix bug reading config on first load + 3.42.49 2016-04-09 ================== Cleaned up output when installing v4 CLI diff --git a/Gemfile.lock b/Gemfile.lock index 8cb832f30..fa161763a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.49) + heroku (3.42.50) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index d19901426..4d5976101 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.49" + VERSION = "3.42.50" end From 781f16d9b52349983edbd715608cb14ec7197a54 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 13 Apr 2016 12:04:22 -0400 Subject: [PATCH 898/952] use SHA512 --- tasks/deb.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/deb.rake b/tasks/deb.rake index a163d62d4..50ef856ed 100644 --- a/tasks/deb.rake +++ b/tasks/deb.rake @@ -16,7 +16,7 @@ namespace :deb do sh "apt-ftparchive packages . > Packages" sh "gzip -c Packages > Packages.gz" sh "apt-ftparchive -c #{resource("deb/heroku-toolbelt/apt-ftparchive.conf")} release . > Release" - sh "gpg -abs -u 0F1B0520 -o Release.gpg Release" + sh "gpg --digest-algo SHA512 -abs -u 0F1B0520 -o Release.gpg Release" end end From ded3b24ff9328148af9b72cbadd1581c58ea858a Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sun, 17 Apr 2016 22:04:53 -0700 Subject: [PATCH 899/952] typo --- lib/heroku/helpers.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers.rb b/lib/heroku/helpers.rb index 0890d5aa1..2f1ce94e7 100644 --- a/lib/heroku/helpers.rb +++ b/lib/heroku/helpers.rb @@ -10,7 +10,7 @@ module Helpers def home_directory if running_on_windows? # This used to be File.expand_path("~"), which should have worked but there was a bug - # when a user has a cryllic character in their username. Their username gets mangled + # when a user has a cyrillic character in their username. Their username gets mangled # by a C code operation that does not respect multibyte characters # # see: https://github.com/ruby/ruby/blob/v2_2_3/win32/file.c#L47 From 852709753a5806fd569156554f671345a5076721 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 21 Apr 2016 09:40:06 -0700 Subject: [PATCH 900/952] use dashboard when no arguments are passed (#1933) --- lib/heroku/jsplugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index a18f8999c..5cc6b981e 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -5,6 +5,7 @@ class Heroku::JSPlugin extend Heroku::Helpers def self.try_takeover(command, args) + run('dashboard', nil, []) if ARGV.length == 0 if command == 'help' && args.length > 0 return elsif args.include?('--help') || args.include?('-h') From c2ae02dd3323539a997a700eaa42248cfdd93a16 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 21 Apr 2016 11:49:46 -0500 Subject: [PATCH 901/952] Merge pull request #1936 from heroku/use-excon-redirect-and-expects Fix bug where bad http status code are suppressed --- lib/heroku/excon.rb | 11 ----------- lib/heroku/updater.rb | 7 +++++-- 2 files changed, 5 insertions(+), 13 deletions(-) delete mode 100644 lib/heroku/excon.rb diff --git a/lib/heroku/excon.rb b/lib/heroku/excon.rb deleted file mode 100644 index 2c95b365b..000000000 --- a/lib/heroku/excon.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Excon - - def self.get_with_redirect(url, options={}) - res = Excon.get(url, options) - if [301, 302].include?(res.status) - return self.get_with_redirect(res.headers["Location"], options) - end - res - end - -end diff --git a/lib/heroku/updater.rb b/lib/heroku/updater.rb index f2b7f6acd..ef7e7f4e7 100644 --- a/lib/heroku/updater.rb +++ b/lib/heroku/updater.rb @@ -32,8 +32,11 @@ def self.official_zip_hash def self.http_get(url) require 'excon' - require 'heroku/excon' - Excon.get_with_redirect(url, :nonblock => false).body + Excon.get(url, + :nonblock => false, + :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower], + :expects => [200, 301, 302] + ).body end def self.latest_local_version From adb511be999fb516165ac28c901f6e2e4307c194 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 21 Apr 2016 10:02:16 -0700 Subject: [PATCH 902/952] show progress when installing v4 (#1927) --- lib/heroku/jsplugin.rb | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 5cc6b981e..227db367b 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -134,20 +134,33 @@ def self.setup $stderr.print "heroku-cli: Installing Toolbelt v4..." FileUtils.mkdir_p File.dirname(bin) copy_ca_cert - opts = excon_opts.merge( - :middlewares => Excon.defaults[:middlewares] + [Excon::Middleware::Decompress], - :read_timeout => 300, - ) - resp = Excon.get(url, opts) - open(bin, "wb") do |file| - file.write(resp.body) + + open("#{bin}.gz", "wb") do |file| + streamer = lambda do |chunk, remaining_bytes, total_bytes| + file.write(chunk) + $stderr.print "\rheroku-cli: Installing Toolbelt v4... #{((total_bytes-remaining_bytes)/1000.0/1000).round(2)}MB/#{(total_bytes/1000.0/1000).round(2)}MB" + end + opts = excon_opts.merge( + :chunk_size => 324000, + :read_timeout => 300, + :response_block => streamer + ) + Excon.get(url, opts) + end + + Zlib::GzipReader.open("#{bin}.gz") do |gz| + File.open(bin, "wb") do |file| + IO.copy_stream gz, file + end end + File.delete("#{bin}.gz") + File.chmod(0755, bin) if Digest::SHA1.file(bin).hexdigest != manifest['builds'][os][arch]['sha1'] File.delete bin raise 'SHA mismatch for heroku-cli' end - $stderr.puts " done" + $stderr.puts version end From cb58c818ed4bfd9e59e8eaaa4c669efc2e58e2d8 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 21 Apr 2016 10:17:06 -0700 Subject: [PATCH 903/952] v3.43.0 --- CHANGELOG | 7 +++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index dbecfc4db..f294bf66c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.43.0 2016-04-21 +================== +Added "dashboard" to be displayed when calling `heroku` with no args +Show progress when installing v4 +Fix handling of bad HTTP codes when installing v4 +Use SHA512 to sign Debian package + 3.42.50 2016-04-09 ================== Fix bug reading config on first load diff --git a/Gemfile.lock b/Gemfile.lock index fa161763a..dcb044550 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.42.50) + heroku (3.43.0) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 4d5976101..e392ec00d 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.42.50" + VERSION = "3.43.0" end From f5ab369cc44debf1a6d8b739e92c1c6c47b33dd4 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Tue, 3 May 2016 10:53:23 -0500 Subject: [PATCH 904/952] Exit non-zero code when pg:backups transfer fails (#1943) --- lib/heroku/command/pg_backups.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index e3dd95004..0e5be4269 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -482,6 +482,7 @@ def poll_transfer(action, transfer_id, interval) Please run `heroku pg:backups info #{transfer_name(backup)}` for details. EOF + exit(1) end end From 5687f8061de0eb08eef5dfedd1dc20fcd865e912 Mon Sep 17 00:00:00 2001 From: Keiko Oda Date: Tue, 3 May 2016 11:41:48 -0700 Subject: [PATCH 905/952] Check and update the restore_from URL if it is from Dropbox (#1941) --- lib/heroku/command/pg_backups.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 0e5be4269..c7f640015 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -405,7 +405,7 @@ def restore_backup restore_url = nil if restore_from =~ %r{\Ahttps?://} - restore_url = restore_from + restore_url = check_dropboxurl(restore_from) else # assume we're restoring from a backup if restore_from =~ /::/ @@ -671,4 +671,20 @@ def parse_schedule_time(time_str) end { :hour => hour, :timezone => tz } end + + def check_dropboxurl(url) + # Force a dump file to download instead of rendering as HTML file + # by specifying the dl=1 param for Dropbox URL + if url =~ /www.dropbox.com/ && !url.end_with?('dl=1') + if url.end_with?('dl=0') + url.sub('dl=0', 'dl=1') + elsif url.include?('?') + url + '&dl=1' + else + url + '?dl=1' + end + end + + url + end end From 02cd1d98ec846376ec2510ab6763edd5ee7fbc19 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 4 May 2016 11:28:10 -0500 Subject: [PATCH 906/952] v3.43.1 --- CHANGELOG | 5 +++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f294bf66c..01721afff 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.43.1 2016-05-04 +================== +Check and update the restore_from URL if it is from Dropbox +Exit non-zero code when pg:backups transfer fails + 3.43.0 2016-04-21 ================== Added "dashboard" to be displayed when calling `heroku` with no args diff --git a/Gemfile.lock b/Gemfile.lock index dcb044550..571cb085f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.0) + heroku (3.43.1) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index e392ec00d..5a2e88154 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.0" + VERSION = "3.43.1" end From 0160fa0d2261b727a965f436c933ae6e869a9900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Wed, 4 May 2016 14:13:38 -0700 Subject: [PATCH 907/952] Change pgdiagnose endpoint (#1948) * Style improvements * Update pgdiagnose endpoint --- lib/heroku/command/pg.rb | 30 +++++++++++++----------------- lib/heroku/helpers/pg_diagnose.rb | 16 ++++++++-------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index 9341c1cbc..daaa0fed3 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -102,7 +102,6 @@ def promote else @status = "not needed" end - end action "Promoting #{addon['name']} to #{promoted_name}_URL on #{app}" do @@ -356,7 +355,6 @@ def killall puts exec_sql(sql) end - # pg:push # # push from SOURCE_DATABASE to REMOTE_TARGET_DATABASE @@ -412,7 +410,6 @@ def pull end end - # pg:maintenance # # manage maintenance for @@ -426,7 +423,7 @@ def maintenance mode_with_argument = shift_argument || '' mode, mode_argument = mode_with_argument.split('=') - db = shift_argument + db = shift_argument no_maintenance = force? if mode.nil? || db.nil? || !(%w[info run window].include? mode) Heroku::Command.run(current_command, ["--help"]) @@ -452,7 +449,7 @@ def maintenance end when 'window' unless mode_argument =~ /\A[A-Za-z]{3,10} \d\d?:[03]0\z/ - error('Maintenance windows must be "Day HH:MM", where MM is 00 or 30.') + error('Maintenance windows must be "Day HH:MM", where MM is 00 or 30.') end response = hpg_client(attachment).maintenance_window_set(mode_argument) @@ -460,7 +457,6 @@ def maintenance end end - # pg:upgrade REPLICA # # unfollow a database and upgrade it to the latest stable PostgreSQL version @@ -544,7 +540,7 @@ def links styled_header("#{attachments.map(&:config_var).join(", ")} (#{resource})") - next display response[:message] if response.kind_of?(Hash) + next display response[:message] if response.is_a?(Hash) next display "No data sources are linked into this database." if response.empty? response.each do |link| @@ -602,7 +598,7 @@ def links private def humanize(key) - key.to_s.gsub(/_/, ' ').split(" ").map(&:capitalize).join(" ") + key.to_s.tr('_', ' ').split(" ").map(&:capitalize).join(" ") end def resolve_service(name) @@ -689,10 +685,10 @@ def hpg_databases_with_info # Make headers as per heroku/heroku#1605 names = attachments.map(&:config_var) names << 'DATABASE_URL' if attachments.any? { |att| att.primary_attachment? } - name = names. - uniq. - sort_by { |n| n=='DATABASE_URL' ? '{' : n }. # Weight DATABASE_URL last - join(', ') + name = names + .uniq + .sort_by { |n| n=='DATABASE_URL' ? '{' : n } # Weight DATABASE_URL last + .join(', ') mutex.synchronize do db_infos[name] = info @@ -702,7 +698,7 @@ def hpg_databases_with_info threads.map(&:join) @hpg_databases_with_info = db_infos - return @hpg_databases_with_info + @hpg_databases_with_info end def hpg_info(attachment, extended=false) @@ -785,7 +781,7 @@ def exec_sql(sql) def exec_sql_on_uri(sql,uri) begin ENV["PGPASSWORD"] = uri.password - ENV["PGSSLMODE"] = (uri.host == 'localhost' ? 'prefer' : 'require' ) + ENV["PGSSLMODE"] = (uri.host == 'localhost' ? 'prefer' : 'require' ) ENV["PGAPPNAME"] = "#{pgappname} non-interactive" user_part = uri.user ? "-U #{uri.user}" : "" output = `#{psql_cmd} -c "#{sql}" #{user_part} -h #{uri.host} -p #{uri.port || 5432} #{uri.path[1..-1]}` @@ -827,9 +823,9 @@ def find_or_create_non_database_attachment(app) current_addon = current_attachment && current_attachment['addon'] if current_addon - existing = attachments. - select { |att| att['addon']['id'] == current_addon['id'] }. - detect { |att| att['name'] != 'DATABASE' } + existing = attachments + .select { |att| att['addon']['id'] == current_addon['id'] } + .detect { |att| att['name'] != 'DATABASE' } return existing if existing diff --git a/lib/heroku/helpers/pg_diagnose.rb b/lib/heroku/helpers/pg_diagnose.rb index bf6acc128..b8a309ea3 100644 --- a/lib/heroku/helpers/pg_diagnose.rb +++ b/lib/heroku/helpers/pg_diagnose.rb @@ -1,5 +1,6 @@ module Heroku::Helpers::PgDiagnose - DIAGNOSE_URL = ENV.fetch('PGDIAGNOSE_URL', "https://pgdiagnose.herokuapp.com") + DIAGNOSE_URL = ENV.fetch('PGDIAGNOSE_URL', "https://pgdiagnose.herokai.com") + private def run_diagnose(db_id) @@ -52,10 +53,10 @@ def generate_report(db_id) 'database' => attachment.config_var } - return Excon.post("#{DIAGNOSE_URL}/reports", - :expects => [200, 201], - :body => params.to_json, - :headers => {"Content-Type" => "application/json"}) + Excon.post("#{DIAGNOSE_URL}/reports", + :expects => [200, 201], + :body => params.to_json, + :headers => {"Content-Type" => "application/json"}) end def get_metrics(attachment) @@ -86,9 +87,9 @@ def process_checks(status, checks) next if "green" == status results = check['results'] - return unless results && results.size > 0 + break unless results && results.size > 0 - if results.first.kind_of? Array + if results.first.is_a?(Array) puts " " + results.first.map(&:capitalize).join(" ") else display_table( @@ -101,4 +102,3 @@ def process_checks(status, checks) end end end - From 09d54f231b7911d30d06a6f2085be8ae41bd62e0 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 5 May 2016 13:56:22 -0700 Subject: [PATCH 908/952] download v5 directly (#1950) --- lib/heroku/jsplugin.rb | 86 ++++++++++++++++++++---------------- spec/heroku/jsplugin_spec.rb | 2 +- 2 files changed, 49 insertions(+), 39 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 227db367b..080ec7f00 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -112,53 +112,70 @@ def self.version def self.app_dir localappdata = Heroku::Helpers::Env['LOCALAPPDATA'] - xdg_data_home = Heroku::Helpers::Env['XDG_DATA_HOME'] + xdg_data_home = Heroku::Helpers::Env['XDG_DATA_HOME'] || File.join(Heroku::Helpers.home_directory, '.local', 'share') if windows? && localappdata File.join(localappdata, 'heroku') - elsif xdg_data_home - File.join(xdg_data_home, 'heroku') else - File.join(Heroku::Helpers.home_directory, '.heroku') + File.join(xdg_data_home, 'heroku') end end def self.bin - File.join(app_dir, windows? ? 'heroku-cli.exe' : 'heroku-cli') + File.join(app_dir, 'cli', 'bin', windows? ? 'heroku.exe' : 'heroku') end def self.setup check_if_old return if setup? require 'excon' - $stderr.print "heroku-cli: Installing Toolbelt v4..." - FileUtils.mkdir_p File.dirname(bin) - copy_ca_cert - - open("#{bin}.gz", "wb") do |file| - streamer = lambda do |chunk, remaining_bytes, total_bytes| - file.write(chunk) - $stderr.print "\rheroku-cli: Installing Toolbelt v4... #{((total_bytes-remaining_bytes)/1000.0/1000).round(2)}MB/#{(total_bytes/1000.0/1000).round(2)}MB" + require 'rubygems/package' + $stderr.print "heroku-cli: Installing CLI..." + + Dir.mktmpdir do |tmp| + archive = File.join(tmp, "heroku.tar.gz") + open(archive, "wb") do |file| + streamer = lambda do |chunk, remaining_bytes, total_bytes| + file.write(chunk) + $stderr.print "\rheroku-cli: Installing CLI... #{((total_bytes-remaining_bytes)/1000.0/1000).round(2)}MB/#{(total_bytes/1000.0/1000).round(2)}MB" + end + opts = excon_opts.merge( + :chunk_size => 324000, + :read_timeout => 300, + :response_block => streamer + ) + Excon.get(url, opts) end - opts = excon_opts.merge( - :chunk_size => 324000, - :read_timeout => 300, - :response_block => streamer - ) - Excon.get(url, opts) - end - Zlib::GzipReader.open("#{bin}.gz") do |gz| - File.open(bin, "wb") do |file| - IO.copy_stream gz, file + if Digest::SHA256.file(archive).hexdigest != manifest['builds']["#{os}-#{arch}"]['sha256'] + raise 'SHA mismatch for heroku.tar.gz' end - end - File.delete("#{bin}.gz") - File.chmod(0755, bin) - if Digest::SHA1.file(bin).hexdigest != manifest['builds'][os][arch]['sha1'] - File.delete bin - raise 'SHA mismatch for heroku-cli' + FileUtils.mkdir_p(app_dir) + FileUtils.rm_rf(File.join(app_dir, 'cli')) + Zlib::GzipReader.open(archive) do |gz| + Gem::Package::TarReader.new(gz) do |tar| + dest = nil + tar.each do |entry| + if entry.full_name == '././@LongLink' + dest = File.join(app_dir, entry.read.strip.gsub(/^heroku/, 'cli')) + next + end + dest ||= File.join(app_dir, entry.full_name.gsub(/^heroku/, 'cli')) + if entry.directory? + FileUtils.mkdir_p(dest, mode: entry.header.mode) + elsif entry.file? + File.open(dest, 'wb') do |f| + f.print entry.read + end + FileUtils.chmod(entry.header.mode, dest) + elsif entry.header.typeflag == '2' && !windows? + File.symlink entry.header.linkname, dest + end + dest = nil + end + end + end end $stderr.puts version @@ -168,13 +185,6 @@ def self.setup? File.exist? bin end - def self.copy_ca_cert - to = File.join(app_dir, "cacert.pem") - return if File.exists?(to) - from = File.expand_path("../../../data/cacert.pem", __FILE__) - FileUtils.copy(from, to) - end - def self.run(topic, command, args) cmd = command ? "#{topic}:#{command}" : topic bin = self.bin @@ -222,7 +232,7 @@ def self.os end def self.manifest - @manifest ||= JSON.parse(Excon.get("https://cli-assets.heroku.com/master/manifest.json", excon_opts).body) + @manifest ||= JSON.parse(Excon.get("https://cli-assets.heroku.com/branches/stable/gz/manifest.json", excon_opts).body) end def self.excon_opts @@ -235,7 +245,7 @@ def self.excon_opts end def self.url - manifest['builds'][os][arch]['url'] + ".gz" + manifest['builds']["#{os}-#{arch}"]['url'] end def self.find_command(s) diff --git a/spec/heroku/jsplugin_spec.rb b/spec/heroku/jsplugin_spec.rb index 6e99bb605..49df862db 100644 --- a/spec/heroku/jsplugin_spec.rb +++ b/spec/heroku/jsplugin_spec.rb @@ -26,7 +26,7 @@ module Heroku end it "should default to home directory" do - expect(Heroku::JSPlugin.app_dir).to eq(File.join(Heroku::Helpers.home_directory, ".heroku")) + expect(Heroku::JSPlugin.app_dir).to eq(File.join(Heroku::Helpers.home_directory, ".local", "share", "heroku")) end after do From 10c62149e541a328f7c11a9b4f9e317328a2144c Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Thu, 5 May 2016 14:05:43 -0700 Subject: [PATCH 909/952] v3.43.2 --- CHANGELOG | 5 +++++ Gemfile.lock | 4 ++-- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 01721afff..53cb57956 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.43.2 2016-05-05 +================== +Download v5 directly +Change pg:diagnose endpoint + 3.43.1 2016-05-04 ================== Check and update the restore_from URL if it is from Dropbox diff --git a/Gemfile.lock b/Gemfile.lock index 571cb085f..869f73b69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.1) + heroku (3.43.2) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -103,4 +103,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.11.2 + 1.12.1 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 5a2e88154..2d3e34538 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.1" + VERSION = "3.43.2" end From ed7945690909741ec50fcff91e41815b32ff4c6d Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 17 May 2016 09:54:50 -0700 Subject: [PATCH 910/952] show NAME instead of URL for plugins:install --- lib/heroku/command/plugins.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/plugins.rb b/lib/heroku/command/plugins.rb index 720a8b3c4..2c8d8f793 100644 --- a/lib/heroku/command/plugins.rb +++ b/lib/heroku/command/plugins.rb @@ -31,7 +31,7 @@ def index end end - # plugins:install URL + # plugins:install NAME # # install a plugin # From 37a8ab349043dcf7106289cf18648d235a8018f5 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 25 May 2016 09:17:37 -0700 Subject: [PATCH 911/952] retry v5 install (#1958) --- lib/heroku/jsplugin.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 080ec7f00..37d8d91ad 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -144,7 +144,18 @@ def self.setup :read_timeout => 300, :response_block => streamer ) - Excon.get(url, opts) + retries = 5 + begin + Excon.get(url, opts) + rescue => e + if retries > 0 + $stderr.puts "\nError: #{e}\n#{e.backtrace.join("\n")}\n\nretrying...\n" + retries = retries - 1 + retry + else + raise e + end + end end if Digest::SHA256.file(archive).hexdigest != manifest['builds']["#{os}-#{arch}"]['sha256'] From 5a83a8662c96b2f72166c8debc27f1a385b0952d Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 25 May 2016 09:19:25 -0700 Subject: [PATCH 912/952] v3.43.3 --- CHANGELOG | 4 ++++ lib/heroku/version.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 53cb57956..3c76d3216 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.43.3 2016-05-25 +================== +Retry v5 download on failure + 3.43.2 2016-05-05 ================== Download v5 directly diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 2d3e34538..04eaff689 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.2" + VERSION = "3.43.3" end From d42baba1b584e84a524d04c36624a98b34ab5a70 Mon Sep 17 00:00:00 2001 From: takiy33 Date: Thu, 9 Jun 2016 10:58:21 +0900 Subject: [PATCH 913/952] Update heroku from 3.43.2 to 3.43.3 (#1964) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 869f73b69..d12f2906b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.2) + heroku (3.43.3) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) From 883d20b9ae41ce23b8c3a0b5dece7c08a9f19b81 Mon Sep 17 00:00:00 2001 From: Tom Crayford Date: Fri, 24 Jun 2016 14:56:39 +0100 Subject: [PATCH 914/952] repoint pg:diagnose to cedar pgdiagnose (#1971) --- lib/heroku/helpers/pg_diagnose.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers/pg_diagnose.rb b/lib/heroku/helpers/pg_diagnose.rb index b8a309ea3..f5b694f3f 100644 --- a/lib/heroku/helpers/pg_diagnose.rb +++ b/lib/heroku/helpers/pg_diagnose.rb @@ -1,5 +1,5 @@ module Heroku::Helpers::PgDiagnose - DIAGNOSE_URL = ENV.fetch('PGDIAGNOSE_URL', "https://pgdiagnose.herokai.com") + DIAGNOSE_URL = ENV.fetch('PGDIAGNOSE_URL', "https://pgdiagnose.herokuapp.com") private From b3ad1b1e8d3c3bbddb9a05b37acd4ec991d00967 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 24 Jun 2016 06:58:02 -0700 Subject: [PATCH 915/952] v3.43.4 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3c76d3216..3e72b7739 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.43.4 2016-06-24 +================== +Update pgdiagnose hostname + 3.43.3 2016-05-25 ================== Retry v5 download on failure diff --git a/Gemfile.lock b/Gemfile.lock index d12f2906b..9bef68d17 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.3) + heroku (3.43.4) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 04eaff689..754485aff 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.3" + VERSION = "3.43.4" end From 6738688170017765b2fbce75e222904cceca0828 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 29 Jun 2016 11:25:04 -0700 Subject: [PATCH 916/952] remove drains code --- lib/heroku/command/drains.rb | 45 ------------------------------------ 1 file changed, 45 deletions(-) delete mode 100644 lib/heroku/command/drains.rb diff --git a/lib/heroku/command/drains.rb b/lib/heroku/command/drains.rb deleted file mode 100644 index f3cd75f81..000000000 --- a/lib/heroku/command/drains.rb +++ /dev/null @@ -1,45 +0,0 @@ -require "heroku/command/base" - -module Heroku::Command - - # display drains for an app - # - class Drains < Base - - # drains - # - # list all drains - # - def index - puts heroku.list_drains(app) - return - end - - # drains:add URL - # - # add a drain - # - def add - if url = args.shift - puts heroku.add_drain(app, url) - return - else - error("Usage: heroku drains:add URL") - end - end - - # drains:remove URL - # - # remove a drain - # - def remove - if url = args.shift - puts heroku.remove_drain(app, url) - return - else - error("Usage: heroku drains remove URL") - end - end - - end -end From b9fe229ada20841dedcec56d177963061f55273e Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 29 Jun 2016 11:33:45 -0700 Subject: [PATCH 917/952] Revert "repoint pg:diagnose to cedar pgdiagnose (#1971)" This reverts commit 883d20b9ae41ce23b8c3a0b5dece7c08a9f19b81. --- lib/heroku/helpers/pg_diagnose.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/helpers/pg_diagnose.rb b/lib/heroku/helpers/pg_diagnose.rb index f5b694f3f..b8a309ea3 100644 --- a/lib/heroku/helpers/pg_diagnose.rb +++ b/lib/heroku/helpers/pg_diagnose.rb @@ -1,5 +1,5 @@ module Heroku::Helpers::PgDiagnose - DIAGNOSE_URL = ENV.fetch('PGDIAGNOSE_URL', "https://pgdiagnose.herokuapp.com") + DIAGNOSE_URL = ENV.fetch('PGDIAGNOSE_URL', "https://pgdiagnose.herokai.com") private From e35fdc89b4ea167a7278d5197e8799472e8af3d8 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Wed, 29 Jun 2016 11:51:32 -0700 Subject: [PATCH 918/952] remove drains specs --- spec/heroku/command/drains_spec.rb | 34 ------------------------------ 1 file changed, 34 deletions(-) delete mode 100644 spec/heroku/command/drains_spec.rb diff --git a/spec/heroku/command/drains_spec.rb b/spec/heroku/command/drains_spec.rb deleted file mode 100644 index 629f88ce8..000000000 --- a/spec/heroku/command/drains_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require "spec_helper" -require "heroku/command/drains" - -describe Heroku::Command::Drains do - - describe "drains" do - it "can list drains" do - stub_core.list_drains("example").returns("drains") - stderr, stdout = execute("drains") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -drains -STDOUT - end - - it "can add drains" do - stub_core.add_drain("example", "syslog://localhost/add").returns("added") - stderr, stdout = execute("drains:add syslog://localhost/add") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -added -STDOUT - end - - it "can remove drains" do - stub_core.remove_drain("example", "syslog://localhost/remove").returns("removed") - stderr, stdout = execute("drains:remove syslog://localhost/remove") - expect(stderr).to eq("") - expect(stdout).to eq <<-STDOUT -removed -STDOUT - end - end -end From 2f5438021d18f620acd0709fe667b72fcd529fae Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 30 Jun 2016 09:55:45 -0700 Subject: [PATCH 919/952] Add libssl1.0.2 to the debian dependencies (#1974) --- resources/deb/heroku/control | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/deb/heroku/control b/resources/deb/heroku/control index bbe07e573..3b69db8d6 100644 --- a/resources/deb/heroku/control +++ b/resources/deb/heroku/control @@ -3,6 +3,6 @@ Version: <%= version %> Section: main Priority: standard Architecture: all -Depends: ruby2.3|ruby2.2|ruby2.1|ruby2.0|libopenssl-ruby1.9.1, ruby2.3|ruby2.2|ruby2.1|ruby2.0|libreadline-ruby1.9.1, ruby2.3|ruby2.2|ruby2.1|ruby2.0|ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0 +Depends: ruby2.3|ruby2.2|ruby2.1|ruby2.0|libopenssl-ruby1.9.1, ruby2.3|ruby2.2|ruby2.1|ruby2.0|libreadline-ruby1.9.1, ruby2.3|ruby2.2|ruby2.1|ruby2.0|ruby1.9.1, libssl0.9.8 (>= 0.9.8k) | libssl1.0.0|libssl1.0.2 Maintainer: Heroku Description: Client library and CLI to deploy apps on Heroku. From a278ada12ae8014901dc199dca22956c69a49f7a Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 30 Jun 2016 13:09:02 -0500 Subject: [PATCH 920/952] v3.43.5 --- CHANGELOG | 6 ++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3e72b7739..b489d4097 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +3.43.5 2016-06-30 +================== +Add libssl1.0.2 to the debian dependencies +Remove drains code +Revert "repoint pg:diagnose to cedar pgdiagnose (#1971)" + 3.43.4 2016-06-24 ================== Update pgdiagnose hostname diff --git a/Gemfile.lock b/Gemfile.lock index 9bef68d17..58482d55c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.4) + heroku (3.43.5) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 754485aff..1dc83edb8 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.4" + VERSION = "3.43.5" end From 638036ed0ae7d8a896903b1aee2f7531da9131e6 Mon Sep 17 00:00:00 2001 From: Camille Baldock Date: Mon, 18 Jul 2016 19:51:17 +0100 Subject: [PATCH 921/952] Remove special cases for 9.1 support (#1981) --- lib/heroku/command/pg.rb | 51 +++++++++++----------------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/lib/heroku/command/pg.rb b/lib/heroku/command/pg.rb index daaa0fed3..1417e2c93 100644 --- a/lib/heroku/command/pg.rb +++ b/lib/heroku/command/pg.rb @@ -286,34 +286,32 @@ def ps requires_preauth sql = %Q( SELECT - #{pid_column}, - #{"state," if nine_two?} + pid, + state, application_name AS source, age(now(),xact_start) AS running_for, waiting, - #{query_column} AS query + query FROM pg_stat_activity WHERE - #{query_column} <> '' + query <> '' #{ # Apply idle-backend filter appropriate to versions and options. case when options[:verbose] '' - when nine_two? - "AND state <> 'idle'" else - "AND current_query <> ''" + "AND state <> 'idle'" end } - AND #{pid_column} <> pg_backend_pid() + AND pid <> pg_backend_pid() ORDER BY query_start DESC ) puts exec_sql(sql) end - # pg:kill procpid [DATABASE] + # pg:kill pid [DATABASE] # # kill a query # @@ -321,12 +319,12 @@ def ps # def kill requires_preauth - procpid = shift_argument - output_with_bang "procpid to kill is required" unless procpid && procpid.to_i != 0 - procpid = procpid.to_i + pid = shift_argument + output_with_bang "procpid to kill is required" unless pid && pid.to_i != 0 + pid = pid.to_i cmd = force? ? 'pg_terminate_backend' : 'pg_cancel_backend' - sql = %Q(SELECT #{cmd}(#{procpid});) + sql = %Q(SELECT #{cmd}(#{pid});) puts exec_sql(sql) end @@ -346,10 +344,10 @@ def killall # fall back to original mechanism if calling the reset endpoint # fails sql = %Q( - SELECT pg_terminate_backend(#{pid_column}) + SELECT pg_terminate_backend(pid) FROM pg_stat_activity - WHERE #{pid_column} <> pg_backend_pid() - AND #{query_column} <> '' + WHERE pid <> pg_backend_pid() + AND query <> '' ) puts exec_sql(sql) @@ -750,27 +748,6 @@ def version @version = result[1] end - def nine_two? - return @nine_two if defined? @nine_two - @nine_two = version.to_f >= 9.2 - end - - def pid_column - if nine_two? - 'pid' - else - 'procpid' - end - end - - def query_column - if nine_two? - 'query' - else - 'current_query' - end - end - def exec_sql(sql) @attachment ||= generate_resolver.resolve(shift_argument, "DATABASE_URL") @attachment.maybe_tunnel do |uri| From 10576c1ce88e242565d53ba89f71f30e78117179 Mon Sep 17 00:00:00 2001 From: Keiko Oda Date: Mon, 18 Jul 2016 11:51:35 -0700 Subject: [PATCH 922/952] Improve pg:backups schedule time validation (#1978) --- lib/heroku/command/pg_backups.rb | 4 +-- spec/heroku/command/pg_backups_spec.rb | 34 +++++++++++++++++--------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index c7f640015..126ee0f06 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -646,8 +646,8 @@ def hpg_app_client(app_name) end def parse_schedule_time(time_str) - hour, tz = time_str.match(/([0-2][0-9]):00 ?(.*)/) && [ $1, $2 ] - if hour.nil? || tz.nil? + hour, tz = time_str.match(/^([0-2][0-9]):00 ?(\S*)$/) && [ $1, $2 ] + if hour.nil? || tz.nil? || hour.empty? || tz.empty? abort("Invalid schedule format: expected ':00 '") end # do-what-i-mean remapping, since transferatu is (rightfully) picky diff --git a/spec/heroku/command/pg_backups_spec.rb b/spec/heroku/command/pg_backups_spec.rb index 32c0d1a21..636cfc366 100644 --- a/spec/heroku/command/pg_backups_spec.rb +++ b/spec/heroku/command/pg_backups_spec.rb @@ -240,7 +240,7 @@ module Heroku::Command end context "demonstrating cultural imperialism" do - { + { 'PST' => 'America/Los_Angeles', 'PDT' => 'America/Los_Angeles', 'MST' => 'America/Boise', @@ -252,16 +252,28 @@ module Heroku::Command 'Z' => 'UTC', 'GMT' => 'Europe/London', 'BST' => 'Europe/London', - }.each do |common_but_ambiguous_abbreviation, official_tz_db_name| - it "translates #{common_but_ambiguous_abbreviation} to #{official_tz_db_name}" do - stub_pg.schedule({ hour: '07', timezone: official_tz_db_name, - schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) - specified_time = "07:00 #{common_but_ambiguous_abbreviation}" - stderr, stdout = execute("pg:backups schedule RED --at '#{specified_time}' --app example") - expect(stderr).to be_empty - expect(stdout).to match(/Scheduled automatic daily backups/) - end - end + }.each do |common_but_ambiguous_abbreviation, official_tz_db_name| + it "translates #{common_but_ambiguous_abbreviation} to #{official_tz_db_name}" do + stub_pg.schedule({ hour: '07', timezone: official_tz_db_name, + schedule_name: 'HEROKU_POSTGRESQL_RED_URL' }) + specified_time = "07:00 #{common_but_ambiguous_abbreviation}" + stderr, stdout = execute("pg:backups schedule RED --at '#{specified_time}' --app example") + expect(stderr).to be_empty + expect(stdout).to match(/Scheduled automatic daily backups/) + end + end + end + + [ + "02:00:00 America/Bogota", + "12:00am EST", + "04:00" + ].each do |at| + it "complains when called with invalid at" do + stderr, stdout = execute("pg:backups schedule RED --at '#{at}' --app example") + expect(stderr).to match(/Invalid schedule format: expected ':00 '/) + expect(stdout).to be_empty + end end end From a9d1afbfb3e40728efb8137795d5a9e458a10911 Mon Sep 17 00:00:00 2001 From: Keiko Oda Date: Mon, 18 Jul 2016 11:51:53 -0700 Subject: [PATCH 923/952] Ensure the database is empty regardless the LANG setting (#1977) --- lib/heroku/helpers/pg_dump_restore.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/heroku/helpers/pg_dump_restore.rb b/lib/heroku/helpers/pg_dump_restore.rb index 68e5ce371..87e7aaea5 100644 --- a/lib/heroku/helpers/pg_dump_restore.rb +++ b/lib/heroku/helpers/pg_dump_restore.rb @@ -55,9 +55,9 @@ def create_local_db end def ensure_remote_db_empty - sql = 'select count(*) = 0 from pg_stat_user_tables;' + sql = 'select count(*) = 0 as empty from pg_stat_user_tables;' result = exec_sql_on_uri(sql, @target) - unless result == " ?column? \n----------\n t\n(1 row)\n\n" + unless result.start_with? " empty \n-------\n t" command.error("Remote database is not empty.\nPlease create a new database, or use `heroku pg:reset`") end end From dff3ac22a718a3cf62412da22fc477d03c5eff68 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 22 Jul 2016 09:19:29 -0700 Subject: [PATCH 924/952] ignore --help commands in analytics --- lib/heroku/analytics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index aa16ce59a..457e5b7cb 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -51,6 +51,7 @@ def self.submit_analytics(user, commands, path) def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' + return true if ARGV.include? "--help" return true unless user if Heroku::Config[:skip_analytics] == nil From 7a3fdbf972a47b28716769437a39734b8e8b7ee5 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 22 Jul 2016 09:51:22 -0700 Subject: [PATCH 925/952] v3.43.6 --- CHANGELOG | 7 +++++++ Gemfile.lock | 6 +++--- lib/heroku/version.rb | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b489d4097..880833198 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,10 @@ +3.43.6 2016-07-22 +================== +Ensure database is empty regardless of LANG setting +Improve pg:backups schedule time validation +Remove special cases for pg9.1 +Ignore --help commands in analytics + 3.43.5 2016-06-30 ================== Add libssl1.0.2 to the debian dependencies diff --git a/Gemfile.lock b/Gemfile.lock index 58482d55c..82cfe0fde 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.5) + heroku (3.43.6) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -31,7 +31,7 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - excon (0.47.0) + excon (0.51.0) fakefs (0.6.7) heroku-api (0.4.2) excon (~> 0.45) @@ -103,4 +103,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.12.1 + 1.12.5 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 1dc83edb8..dfd7cc3bb 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.5" + VERSION = "3.43.6" end From 637656012444ab8425cd31f1ca148f6512b518f4 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 22 Jul 2016 20:39:11 -0700 Subject: [PATCH 926/952] ignore -h commands in analytics --- lib/heroku/analytics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 457e5b7cb..9ffbc22ce 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -52,6 +52,7 @@ def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' return true if ARGV.include? "--help" + return true if ARGV.include? "-h" return true unless user if Heroku::Config[:skip_analytics] == nil From 7d08730d8af642441d6df7650fc0b4b465347f65 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 22 Jul 2016 20:40:20 -0700 Subject: [PATCH 927/952] v3.43.7 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 82cfe0fde..39f7275fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.6) + heroku (3.43.7) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index dfd7cc3bb..9645b3d18 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.6" + VERSION = "3.43.7" end From 3d9cf69964e2d3735bd3e08090c2a6e6c2a67832 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 30 Jul 2016 16:24:57 -0700 Subject: [PATCH 928/952] new analytics schema --- lib/heroku/analytics.rb | 45 ++++++++++++++--------------------------- lib/heroku/cli.rb | 1 - lib/heroku/command.rb | 1 + lib/heroku/plugin.rb | 8 ++++++++ 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/lib/heroku/analytics.rb b/lib/heroku/analytics.rb index 9ffbc22ce..0fd8e383f 100644 --- a/lib/heroku/analytics.rb +++ b/lib/heroku/analytics.rb @@ -5,49 +5,26 @@ class Heroku::Analytics def self.record(command) return if skip_analytics - commands = json_decode(File.read(path)) || [] rescue [] + file = json_decode(File.read(path)) || new_file rescue new_file c = Heroku::Command.parse(command) return if c && c[:js] - commands << { + file["commands"] ||= [] + file["commands"] << { command: command, timestamp: Time.now.to_i, - cli_version: Heroku.user_agent, version: Heroku::VERSION, os: Heroku::JSPlugin.os, arch: Heroku::JSPlugin.arch, - language: "ruby/#{RUBY_VERSION}", + language: "ruby", valid: !!c, + plugin: c[:plugin], } - File.open(path, 'w') { |f| f.write(json_encode(commands)) } - rescue - end - - def self.submit - return if skip_analytics - commands = json_decode(File.read(path)) - return if commands.count < 10 # only submit if we have 10 entries to send - begin - fork do - submit_analytics(user, commands, path) - end - rescue NotImplementedError - # cannot fork on windows - submit_analytics(user, commands, path) - end + File.open(path, 'w') { |f| f.write(json_encode(file)) } rescue end private - def self.submit_analytics(user, commands, path) - payload = { - user: user, - commands: commands, - } - Excon.post('https://cli-analytics.heroku.com/record', body: JSON.dump(payload)) - File.truncate(path, 0) - end - def self.skip_analytics return true if ['1', 'true'].include?(ENV['HEROKU_SKIP_ANALYTICS']) return true if ENV['CODESHIP'] == 'true' @@ -65,7 +42,11 @@ def self.skip_analytics end def self.path - File.join(Heroku::Helpers.home_directory, ".heroku", "analytics.json") + home = Heroku::Helpers.home_directory + cache = Heroku::Helpers::Env['XDG_CACHE_HOME'] + cache ||= File.join(Heroku::Helpers::Env['LOCALAPPDATA'], 'heroku') if Heroku::JSPlugin.windows? + cache ||= File.join(home, '.cache', 'heroku') + File.join(cache, "analytics.json") end def self.user @@ -73,4 +54,8 @@ def self.user return unless credentials credentials[0] == '' ? nil : credentials[0] end + + def self.new_file + {schema: 1} + end end diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 34387f25a..f07c67660 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -32,7 +32,6 @@ def self.start(*args) Heroku::Analytics.record(command) warn_if_using_heroku_accounts Heroku::Command.run(command, args) - Heroku::Analytics.submit Heroku::Updater.autoupdate rescue Errno::EPIPE => e error(e.message) diff --git a/lib/heroku/command.rb b/lib/heroku/command.rb index 5b8f85afc..5ed105646 100644 --- a/lib/heroku/command.rb +++ b/lib/heroku/command.rb @@ -44,6 +44,7 @@ def self.namespaces end def self.register_command(command) + command[:plugin] = $currently_loading_plugin commands[command[:command]] = command end diff --git a/lib/heroku/plugin.rb b/lib/heroku/plugin.rb index 0d7cb320a..96ca49490 100644 --- a/lib/heroku/plugin.rb +++ b/lib/heroku/plugin.rb @@ -51,10 +51,12 @@ def self.list def self.load! list.each do |plugin| + $currently_loading_plugin = plugin_remote(plugin) rescue plugin check_for_deprecation(plugin) next if skip_plugins.include?(plugin) load_plugin(plugin) end + $currently_loading_plugin = nil end def self.load_plugin(plugin) @@ -93,6 +95,12 @@ def self.skip_plugins @skip_plugins ||= ENV["SKIP_PLUGINS"].to_s.split(/[ ,]/) end + def self.plugin_remote(plugin) + Dir.chdir(File.join(directory, plugin)) do + git('config --get remote.origin.url') + end + end + def initialize(uri) @uri = uri guess_name(uri) From f57d70c12154debe86301c4ab167fe966941baf8 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 30 Jul 2016 16:32:04 -0700 Subject: [PATCH 929/952] v3.43.8 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 880833198..21e125221 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.43.8 2016-07-30 +================== +New analytics schema + 3.43.6 2016-07-22 ================== Ensure database is empty regardless of LANG setting diff --git a/Gemfile.lock b/Gemfile.lock index 39f7275fa..40aff2c59 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.7) + heroku (3.43.8) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9645b3d18..29f7ca0d9 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.7" + VERSION = "3.43.8" end From d70c10c022c5a9cd9969de7cb6fa7cec7afd2249 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 30 Jul 2016 22:17:54 -0700 Subject: [PATCH 930/952] use same analytics directory --- lib/heroku/config.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/heroku/config.rb b/lib/heroku/config.rb index 888971234..c4b226791 100644 --- a/lib/heroku/config.rb +++ b/lib/heroku/config.rb @@ -23,6 +23,10 @@ def self.config end def self.path - File.join(Heroku::Helpers.home_directory, ".heroku", "config.json") + home = Heroku::Helpers.home_directory + config = Heroku::Helpers::Env['XDG_CONFIG_HOME'] + config ||= File.join(Heroku::Helpers::Env['LOCALAPPDATA'], 'heroku') if Heroku::JSPlugin.windows? + config ||= File.join(home, '.config', 'heroku') + File.join(config, "config.json") end end From 61abb2fbeaae3e9f69e564f096f6f312237bd25e Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sat, 30 Jul 2016 23:01:19 -0700 Subject: [PATCH 931/952] fix analytics with default commands --- lib/heroku/jsplugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 37d8d91ad..786169064 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -260,7 +260,7 @@ def self.url end def self.find_command(s) - commands.find { |c| c[:command] == s } + commands.find { |c| c[:command] == s || (c[:default] && c[:namespace] == s) } end # check if release is one that isn't updateable From 7dd78df9af706089a687aac825c9bbe75f20b115 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sun, 31 Jul 2016 08:03:13 -0700 Subject: [PATCH 932/952] v3.43.9 --- Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 40aff2c59..04e80df57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.8) + heroku (3.43.9) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 29f7ca0d9..9c1a19d51 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.8" + VERSION = "3.43.9" end From ea94a9ea03c29b05fee76e9430448f78d503a502 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sun, 31 Jul 2016 13:36:27 -0700 Subject: [PATCH 933/952] take out auth command to fallback to help --- lib/heroku/command/auth.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/heroku/command/auth.rb b/lib/heroku/command/auth.rb index 26b8cdd4c..6bd8ed0cd 100644 --- a/lib/heroku/command/auth.rb +++ b/lib/heroku/command/auth.rb @@ -4,15 +4,6 @@ # class Heroku::Command::Auth < Heroku::Command::Base - # auth - # - # Authenticate, display token and current user - def index - validate_arguments! - - Heroku::Command::Help.new.send(:help_for_command, current_command) - end - # auth:login # # log in with your heroku credentials From 6b9cb497d73fbef01f940797eb34a487a3bbc606 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Sun, 31 Jul 2016 13:43:14 -0700 Subject: [PATCH 934/952] take out auth index spec --- spec/heroku/command/auth_spec.rb | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/spec/heroku/command/auth_spec.rb b/spec/heroku/command/auth_spec.rb index b8ff58807..ec73c49bf 100644 --- a/spec/heroku/command/auth_spec.rb +++ b/spec/heroku/command/auth_spec.rb @@ -2,19 +2,7 @@ require "heroku/command/auth" describe Heroku::Command::Auth do - describe "auth" do - it "displays heroku help auth" do - stderr, stdout = execute("auth") - - expect(stderr).to eq("") - expect(stdout).to include "Additional commands" - expect(stdout).to include "auth:login" - expect(stdout).to include "auth:logout" - end - end - describe "auth:token" do - it "displays the user's api key" do stderr, stdout = execute("auth:token") expect(stderr).to eq("") From 150d097e0f3241fc99d68ca8c10cafd348184623 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 2 Aug 2016 10:42:21 -0700 Subject: [PATCH 935/952] take config:add step out of release (#1991) --- RELEASE.md | 1 + tasks/zip.rake | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index f6d90eaee..c41149204 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -14,6 +14,7 @@ This is the normal guide on how to do a release. If you are not a member of the * Commit the changes `git commit -m "vX.Y.Z" -a` * Push changes to master `git push origin master` * Go to the buildserver and release http://cli-build.herokai.com/. [Here is the code for the buildserver.](https://github.com/heroku/toolbelt-build-server) +* Run `heroku config:add` command in build output. * [optional] Release the OSX pkg (instructions in [full release guide](./RELEASE-FULL.md)) * [optional] Release the WIN pkg (instructions in [full release guide](./RELEASE-FULL.md)) diff --git a/tasks/zip.rake b/tasks/zip.rake index 82c7a6c39..78fb4f9ab 100644 --- a/tasks/zip.rake +++ b/tasks/zip.rake @@ -13,7 +13,7 @@ namespace :zip do s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client-beta.zip" if beta? s3_store dist("heroku-#{version}.zip"), "heroku-client/heroku-client.zip" unless beta? - sh "heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" unless beta? + puts "RUN THIS: heroku config:add UPDATE_HASH=#{zip_signature} -a toolbelt" unless beta? end file dist("heroku-#{version}.zip") => distribution_files("zip") do |t| From 4709868ab5ebe06dc36d74cb28390a4ea3480f39 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Tue, 9 Aug 2016 11:31:40 -0700 Subject: [PATCH 936/952] set commands to always run as ruby (#1995) right now these are ignored because they are flagged as hidden in the new CLI. Really though we don't want these hidden, so adding this exception in allows us to show the commands in the new CLI and still have the old one keep the old functionality. --- lib/heroku/jsplugin.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 786169064..3a828fea4 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -1,10 +1,19 @@ require 'rbconfig' require 'heroku/helpers/env' +ALWAYS_RUBY_COMMANDS = [ + 'plugins', + 'plugins:install', + 'plugins:uninstall', + 'version', + 'update', +] + class Heroku::JSPlugin extend Heroku::Helpers def self.try_takeover(command, args) + return if ALWAYS_RUBY_COMMANDS.include?(command) run('dashboard', nil, []) if ARGV.length == 0 if command == 'help' && args.length > 0 return From 66fcd1c978610a3a94d5f02360ae4468eafa967c Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Mon, 29 Aug 2016 08:19:51 -0700 Subject: [PATCH 937/952] fix location of config file (#1996) --- lib/heroku/config.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/heroku/config.rb b/lib/heroku/config.rb index c4b226791..89d7195ac 100644 --- a/lib/heroku/config.rb +++ b/lib/heroku/config.rb @@ -25,8 +25,8 @@ def self.config def self.path home = Heroku::Helpers.home_directory config = Heroku::Helpers::Env['XDG_CONFIG_HOME'] - config ||= File.join(Heroku::Helpers::Env['LOCALAPPDATA'], 'heroku') if Heroku::JSPlugin.windows? - config ||= File.join(home, '.config', 'heroku') - File.join(config, "config.json") + config ||= Heroku::Helpers::Env['LOCALAPPDATA'] if Heroku::JSPlugin.windows? + config ||= File.join(home, '.config') + File.join(config, 'heroku', 'config.json') end end From 048189ec5c6f059a565c2fcea6bc0e4847f4c492 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Mon, 29 Aug 2016 08:20:35 -0700 Subject: [PATCH 938/952] replace heroku-accounts warning with ruby plugin deprecation message (#1993) --- lib/heroku/cli.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index f07c67660..045902414 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -30,7 +30,7 @@ def self.start(*args) Heroku::Git.check_git_version Heroku::Command.load Heroku::Analytics.record(command) - warn_if_using_heroku_accounts + warn_if_ruby_plugin(command) Heroku::Command.run(command, args) Heroku::Updater.autoupdate rescue Errno::EPIPE => e @@ -47,11 +47,11 @@ def self.start(*args) exit(1) end - def self.warn_if_using_heroku_accounts - if defined?(Heroku::Command::Accounts.account) - $stderr.print "Uninstalling deprecated ddollar/heroku-accounts plugin..." - Heroku::Plugin.new('heroku-accounts').uninstall - $stderr.print "Done. Use https://github.com/heroku/heroku-accounts instead." + def self.warn_if_ruby_plugin(command) + c = Heroku::Command.parse(command) + if c && c[:plugin] + $stderr.puts "WARNING: #{c[:plugin]} is a deprecated Ruby plugin" + $stderr.puts "WARNING: Check to see if there is a version of this plugin using the new Node.js Heroku CLI" end end end From 12a4e37925b37c236a6d56bdc240f6fc74c7ef52 Mon Sep 17 00:00:00 2001 From: Tom Crayford Date: Thu, 1 Sep 2016 16:49:26 +0100 Subject: [PATCH 939/952] rescue openssl errors that appear transiently (#2005) caller gets this stacktrace: ``` SSL_connect SYSCALL returned=5 errno=0 state=unknown state /usr/lib/ruby/2.1.0/net/http.rb:920:in `connect' /usr/lib/ruby/2.1.0/net/http.rb:920:in `block in connect' /usr/lib/ruby/2.1.0/timeout.rb:76:in `timeout' /usr/lib/ruby/2.1.0/net/http.rb:920:in `connect' /usr/lib/ruby/2.1.0/net/http.rb:863:in `do_start' /usr/lib/ruby/2.1.0/net/http.rb:852:in `start' /usr/local/heroku/vendor/gems/rest-client-1.6.8/lib/restclient/request.rb:206:in `transmit' /usr/local/heroku/vendor/gems/rest-client-1.6.8/lib/restclient/request.rb:68:in `execute' /usr/local/heroku/vendor/gems/rest-client-1.6.8/lib/restclient/request.rb:35:in `execute' /usr/local/heroku/vendor/gems/rest-client-1.6.8/lib/restclient/resource.rb:51:in `get' /usr/local/heroku/lib/heroku/client/heroku_postgresql_backups.rb:55:in `block (2 levels) in http_get' /usr/local/heroku/lib/heroku/helpers.rb:126:in `retry_on_exception' /usr/local/heroku/lib/heroku/client/heroku_postgresql_backups.rb:54:in `block in http_get' /usr/local/heroku/lib/heroku/client/heroku_postgresql_backups.rb:106:in `checking_client_version' /usr/local/heroku/lib/heroku/client/heroku_postgresql_backups.rb:53:in `http_get' /usr/local/heroku/lib/heroku/client/heroku_postgresql_backups.rb:20:in `transfers_get' /usr/local/heroku/lib/heroku/command/pg_backups.rb:455:in `poll_transfer' /usr/local/heroku/lib/heroku/command/pg_backups.rb:444:in `restore_backup' /usr/local/heroku/lib/heroku/command/pg_backups.rb:81:in `backups' /usr/local/heroku/lib/heroku/command.rb:213:in `run' /usr/local/heroku/lib/heroku/cli.rb:34:in `start' /usr/local/heroku/bin/heroku:25:in `
    ' ``` --- lib/heroku/command/pg_backups.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 126ee0f06..f4c4f1efa 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -461,7 +461,7 @@ def poll_transfer(action, transfer_id, interval) end redisplay status ticks += 1 - rescue RestClient::Exception + rescue RestClient::Exception, OpenSSL::SSL::SSLError backup = {} failed_count += 1 if failed_count > 120 From 50e0f856ab98f0c71d6fb6baf16ba9fec1d5dbd1 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 1 Sep 2016 11:08:26 -0500 Subject: [PATCH 940/952] v3.43.10 --- CHANGELOG | 9 +++++++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 21e125221..e70d67bc5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +3.43.10 2016-09-01 +================== +Take out auth command to fallback to help +Take config:add step out of release +Set commands to always run as ruby +Fix location of config file +Replace heroku-accounts warning with ruby plugin deprecation message +Rescue openssl errors that appear transiently + 3.43.8 2016-07-30 ================== New analytics schema diff --git a/Gemfile.lock b/Gemfile.lock index 04e80df57..d0d593b95 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.9) + heroku (3.43.10) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 9c1a19d51..702b66371 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.9" + VERSION = "3.43.10" end From a46d32569d7e44e0a4099dda9c817b2b1506c421 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 7 Sep 2016 13:10:12 -0500 Subject: [PATCH 941/952] Update rest-client gem because security vulnerabilities (#2011) * Update rest-client gem because security vulnerabilities * add credentials to webmock urls * Use url.parse & Addressable::Template * Fix CI build breakage by mocking when no netrc --- Gemfile.lock | 19 +++++++++----- heroku.gemspec | 2 +- lib/heroku/client.rb | 2 +- spec/heroku/client/ssl_endpoint_spec.rb | 12 ++++----- spec/heroku/client_spec.rb | 33 +++++++++++++++++++------ spec/spec_helper.rb | 11 ++++++--- 6 files changed, 55 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d0d593b95..f9ea58d10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,7 +8,7 @@ PATH net-ssh (= 2.9.2) net-ssh-gateway (= 1.2.0) netrc (= 0.10.3) - rest-client (= 1.6.8) + rest-client (= 1.8.0) rubyzip (= 1.1.7) GEM @@ -31,16 +31,20 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) + domain_name (0.5.20160615) + unf (>= 0.0.5, < 1.0.0) excon (0.51.0) fakefs (0.6.7) heroku-api (0.4.2) excon (~> 0.45) multi_json (~> 1.8) + http-cookie (1.0.2) + domain_name (~> 0.5) json (1.8.2) launchy (2.4.3) addressable (~> 2.3) method_source (0.8.2) - mime-types (1.25.1) + mime-types (2.99.2) multi_json (1.11.2) net-ssh (2.9.2) net-ssh-gateway (1.2.0) @@ -51,10 +55,10 @@ GEM method_source (~> 0.8.1) slop (~> 3.4) rake (10.4.2) - rdoc (4.2.0) - rest-client (1.6.8) - mime-types (~> 1.16) - rdoc (>= 2.4.2) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) rr (1.1.2) rspec (3.2.0) rspec-core (~> 3.2.0) @@ -81,6 +85,9 @@ GEM tins (~> 1.0) thor (0.19.1) tins (1.3.5) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.2) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) diff --git a/heroku.gemspec b/heroku.gemspec index a974ad5a0..c65c8e1a0 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |gem| gem.add_dependency "heroku-api", "0.4.2" gem.add_dependency "launchy", "2.4.3" gem.add_dependency "netrc", "0.10.3" - gem.add_dependency "rest-client", "1.6.8" + gem.add_dependency "rest-client", "1.8.0" gem.add_dependency "rubyzip", "1.1.7" gem.add_dependency "multi_json", "1.11.2" gem.add_dependency "net-ssh-gateway", "1.2.0" diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index 013134679..f2cdafdfc 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -716,7 +716,7 @@ def realize_full_uri(given) def default_resource_options_for_uri(uri) if ENV["HEROKU_SSL_VERIFY"] == "disable" {} - elsif realize_full_uri(uri) =~ %r|^https://api.heroku.com| + elsif URI.parse(realize_full_uri(uri)).host == "api.heroku.com" { :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file } else {} diff --git a/spec/heroku/client/ssl_endpoint_spec.rb b/spec/heroku/client/ssl_endpoint_spec.rb index a78abf49d..e961dda72 100644 --- a/spec/heroku/client/ssl_endpoint_spec.rb +++ b/spec/heroku/client/ssl_endpoint_spec.rb @@ -7,20 +7,20 @@ end it "adds an ssl endpoint" do - stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints"). + stub_api_request(:post, "/apps/example/ssl-endpoints"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_add("example", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) end it "gets info on an ssl endpoint" do - stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). + stub_api_request(:get, "/apps/example/ssl-endpoints/tokyo-1050"). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_info("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "lists ssl endpoints for an app" do - stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints"). + stub_api_request(:get, "/apps/example/ssl-endpoints"). to_return(:body => %{ [{"cname": "tokyo-1050" }, {"cname": "tokyo-1051" }] }) expect(@client.ssl_endpoint_list("example")).to eq([ { "cname" => "tokyo-1050" }, @@ -29,18 +29,18 @@ end it "removes an ssl endpoint" do - stub_request(:delete, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050") + stub_api_request(:delete, "/apps/example/ssl-endpoints/tokyo-1050") @client.ssl_endpoint_remove("example", "tokyo-1050") end it "rolls back an ssl endpoint" do - stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050/rollback"). + stub_api_request(:post, "/apps/example/ssl-endpoints/tokyo-1050/rollback"). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_rollback("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "updates an ssl endpoint" do - stub_request(:put, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). + stub_api_request(:put, "/apps/example/ssl-endpoints/tokyo-1050"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_update("example", "tokyo-1050", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) diff --git a/spec/heroku/client_spec.rb b/spec/heroku/client_spec.rb index d9a8826a9..4ef940a77 100644 --- a/spec/heroku/client_spec.rb +++ b/spec/heroku/client_spec.rb @@ -256,7 +256,7 @@ end it "remove_collaborator(app_name, email) -> removes collaborator from app" do - stub_api_request(:delete, "/apps/example/collaborators/joe%40example%2Ecom") + stub_api_request(:delete, "/apps/example/collaborators/joe@example.com") capture_stderr do # capture deprecation message @client.remove_collaborator('example', 'joe@example.com') end @@ -344,7 +344,7 @@ end it "remove_key(key) -> remove an SSH key by name (user@box)" do - stub_api_request(:delete, "/user/keys/joe%40workstation") + stub_api_request(:delete, "/user/keys/joe@workstation") capture_stderr do # capture deprecation message @client.remove_key('joe@workstation') end @@ -436,7 +436,7 @@ end it "uninstall_addon(app_name, addon_name)" do - stub_api_request(:delete, "/apps/example/addons/addon1?"). + stub_api_request(:delete, "/apps/example/addons/addon1"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) expect(@client.uninstall_addon('example', 'addon1')).to be_truthy @@ -450,7 +450,7 @@ end it "install_addon(app_name, addon_name) with response" do - stub_request(:post, "https://api.heroku.com/apps/example/addons/addon1"). + stub_api_request(:post, "/apps/example/addons/addon1"). to_return(:body => json_encode({'price' => 'free', 'message' => "Don't Panic"})) expect(@client.install_addon('example', 'addon1')). @@ -458,7 +458,7 @@ end it "upgrade_addon(app_name, addon_name) with response" do - stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). + stub_api_request(:put, "/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) expect(@client.upgrade_addon('example', 'addon1')). @@ -466,7 +466,7 @@ end it "downgrade_addon(app_name, addon_name) with response" do - stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). + stub_api_request(:put, "/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) expect(@client.downgrade_addon('example', 'addon1')). @@ -474,7 +474,7 @@ end it "uninstall_addon(app_name, addon_name) with response" do - stub_api_request(:delete, "/apps/example/addons/addon1?"). + stub_api_request(:delete, "/apps/example/addons/addon1"). to_return(:body => json_encode('price'=> 'free', 'message'=> "Don't Panic")) expect(@client.uninstall_addon('example', 'addon1')). @@ -545,4 +545,23 @@ end end end + + describe "default_resource_options_for_uri" do + it "adds verify_ssl and ssl_ca_file for api.heroku.com" do + client = Heroku::Client.new("user", "password") + ssl_ca_file = client.send(:local_ca_file) + expect({ :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => ssl_ca_file}).to eq(client.send(:default_resource_options_for_uri, "https://api.heroku.com")) + end + + it "adds verify_ssl and ssl_ca_file for foo:bar@api.heroku.com" do + client = Heroku::Client.new("user", "password") + ssl_ca_file = client.send(:local_ca_file) + expect({ :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => ssl_ca_file}).to eq(client.send(:default_resource_options_for_uri, "https://foo:bar@api.heroku.com")) + end + + it "does not add verify_ssl and ssl_ca_file for others" do + client = Heroku::Client.new("user", "password") + expect({}).to eq(client.send(:default_resource_options_for_uri, "https://foo:bar@example.com")) + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3ff832d65..3c224a4f7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,7 @@ if (Heroku::Helpers.running_on_windows?) $stdin = File.new("nul") -else +else $stdin = File.new("/dev/null") end @@ -21,6 +21,7 @@ require 'tmpdir' require "webmock/rspec" require "shellwords" +require "netrc" ENV['HEROKU_SKIP_ANALYTICS'] = '1' @@ -38,7 +39,12 @@ def org_api end def stub_api_request(method, path) - stub_request(method, "https://api.heroku.com#{path}") + user, password = Netrc.read["api.heroku.com"] + if (user || password) + stub_request(method, Addressable::Template.new("https://{user}:{pass}@api.heroku.com#{path}")) + else + stub_request(method, "https://api.heroku.com#{path}") + end end def prepare_command(klass) @@ -268,4 +274,3 @@ def self.version; 'heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5' end config.before { Heroku::Helpers.error_with_failure = false } config.after { RR.reset } end - From a57acb0af62925f811844305f3f4bbc8b91ad450 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 7 Sep 2016 13:19:00 -0500 Subject: [PATCH 942/952] v3.43.11 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index e70d67bc5..12fa86a92 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.43.11 2016-09-07 +================== +Update rest-client gem because security vulnerabilities + 3.43.10 2016-09-01 ================== Take out auth command to fallback to help diff --git a/Gemfile.lock b/Gemfile.lock index f9ea58d10..207c044f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.10) + heroku (3.43.11) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 702b66371..00afc9882 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.10" + VERSION = "3.43.11" end From 9f5fb2260c16895e22c4687009548f3404b51200 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 7 Sep 2016 16:53:51 -0500 Subject: [PATCH 943/952] Revert "Update rest-client gem because security vulnerabilities (#2011)" This reverts commit a46d32569d7e44e0a4099dda9c817b2b1506c421. --- Gemfile.lock | 19 +++++--------- heroku.gemspec | 2 +- lib/heroku/client.rb | 2 +- spec/heroku/client/ssl_endpoint_spec.rb | 12 ++++----- spec/heroku/client_spec.rb | 33 ++++++------------------- spec/spec_helper.rb | 11 +++------ 6 files changed, 24 insertions(+), 55 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 207c044f5..46e08edd3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,7 +8,7 @@ PATH net-ssh (= 2.9.2) net-ssh-gateway (= 1.2.0) netrc (= 0.10.3) - rest-client (= 1.8.0) + rest-client (= 1.6.8) rubyzip (= 1.1.7) GEM @@ -31,20 +31,16 @@ GEM safe_yaml (~> 1.0.0) diff-lcs (1.2.5) docile (1.1.5) - domain_name (0.5.20160615) - unf (>= 0.0.5, < 1.0.0) excon (0.51.0) fakefs (0.6.7) heroku-api (0.4.2) excon (~> 0.45) multi_json (~> 1.8) - http-cookie (1.0.2) - domain_name (~> 0.5) json (1.8.2) launchy (2.4.3) addressable (~> 2.3) method_source (0.8.2) - mime-types (2.99.2) + mime-types (1.25.1) multi_json (1.11.2) net-ssh (2.9.2) net-ssh-gateway (1.2.0) @@ -55,10 +51,10 @@ GEM method_source (~> 0.8.1) slop (~> 3.4) rake (10.4.2) - rest-client (1.8.0) - http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) + rdoc (4.2.0) + rest-client (1.6.8) + mime-types (~> 1.16) + rdoc (>= 2.4.2) rr (1.1.2) rspec (3.2.0) rspec-core (~> 3.2.0) @@ -85,9 +81,6 @@ GEM tins (~> 1.0) thor (0.19.1) tins (1.3.5) - unf (0.1.4) - unf_ext - unf_ext (0.0.7.2) webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) diff --git a/heroku.gemspec b/heroku.gemspec index c65c8e1a0..a974ad5a0 100644 --- a/heroku.gemspec +++ b/heroku.gemspec @@ -24,7 +24,7 @@ Gem::Specification.new do |gem| gem.add_dependency "heroku-api", "0.4.2" gem.add_dependency "launchy", "2.4.3" gem.add_dependency "netrc", "0.10.3" - gem.add_dependency "rest-client", "1.8.0" + gem.add_dependency "rest-client", "1.6.8" gem.add_dependency "rubyzip", "1.1.7" gem.add_dependency "multi_json", "1.11.2" gem.add_dependency "net-ssh-gateway", "1.2.0" diff --git a/lib/heroku/client.rb b/lib/heroku/client.rb index f2cdafdfc..013134679 100644 --- a/lib/heroku/client.rb +++ b/lib/heroku/client.rb @@ -716,7 +716,7 @@ def realize_full_uri(given) def default_resource_options_for_uri(uri) if ENV["HEROKU_SSL_VERIFY"] == "disable" {} - elsif URI.parse(realize_full_uri(uri)).host == "api.heroku.com" + elsif realize_full_uri(uri) =~ %r|^https://api.heroku.com| { :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => local_ca_file } else {} diff --git a/spec/heroku/client/ssl_endpoint_spec.rb b/spec/heroku/client/ssl_endpoint_spec.rb index e961dda72..a78abf49d 100644 --- a/spec/heroku/client/ssl_endpoint_spec.rb +++ b/spec/heroku/client/ssl_endpoint_spec.rb @@ -7,20 +7,20 @@ end it "adds an ssl endpoint" do - stub_api_request(:post, "/apps/example/ssl-endpoints"). + stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_add("example", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) end it "gets info on an ssl endpoint" do - stub_api_request(:get, "/apps/example/ssl-endpoints/tokyo-1050"). + stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_info("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "lists ssl endpoints for an app" do - stub_api_request(:get, "/apps/example/ssl-endpoints"). + stub_request(:get, "https://api.heroku.com/apps/example/ssl-endpoints"). to_return(:body => %{ [{"cname": "tokyo-1050" }, {"cname": "tokyo-1051" }] }) expect(@client.ssl_endpoint_list("example")).to eq([ { "cname" => "tokyo-1050" }, @@ -29,18 +29,18 @@ end it "removes an ssl endpoint" do - stub_api_request(:delete, "/apps/example/ssl-endpoints/tokyo-1050") + stub_request(:delete, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050") @client.ssl_endpoint_remove("example", "tokyo-1050") end it "rolls back an ssl endpoint" do - stub_api_request(:post, "/apps/example/ssl-endpoints/tokyo-1050/rollback"). + stub_request(:post, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050/rollback"). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_rollback("example", "tokyo-1050")).to eq({ "cname" => "tokyo-1050" }) end it "updates an ssl endpoint" do - stub_api_request(:put, "/apps/example/ssl-endpoints/tokyo-1050"). + stub_request(:put, "https://api.heroku.com/apps/example/ssl-endpoints/tokyo-1050"). with(:body => { :accept => "json", :pem => "pem content", :key => "key content" }). to_return(:body => %{ {"cname": "tokyo-1050" } }) expect(@client.ssl_endpoint_update("example", "tokyo-1050", "pem content", "key content")).to eq({ "cname" => "tokyo-1050" }) diff --git a/spec/heroku/client_spec.rb b/spec/heroku/client_spec.rb index 4ef940a77..d9a8826a9 100644 --- a/spec/heroku/client_spec.rb +++ b/spec/heroku/client_spec.rb @@ -256,7 +256,7 @@ end it "remove_collaborator(app_name, email) -> removes collaborator from app" do - stub_api_request(:delete, "/apps/example/collaborators/joe@example.com") + stub_api_request(:delete, "/apps/example/collaborators/joe%40example%2Ecom") capture_stderr do # capture deprecation message @client.remove_collaborator('example', 'joe@example.com') end @@ -344,7 +344,7 @@ end it "remove_key(key) -> remove an SSH key by name (user@box)" do - stub_api_request(:delete, "/user/keys/joe@workstation") + stub_api_request(:delete, "/user/keys/joe%40workstation") capture_stderr do # capture deprecation message @client.remove_key('joe@workstation') end @@ -436,7 +436,7 @@ end it "uninstall_addon(app_name, addon_name)" do - stub_api_request(:delete, "/apps/example/addons/addon1"). + stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode({"message" => nil, "price" => "free", "status" => "uninstalled"})) expect(@client.uninstall_addon('example', 'addon1')).to be_truthy @@ -450,7 +450,7 @@ end it "install_addon(app_name, addon_name) with response" do - stub_api_request(:post, "/apps/example/addons/addon1"). + stub_request(:post, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode({'price' => 'free', 'message' => "Don't Panic"})) expect(@client.install_addon('example', 'addon1')). @@ -458,7 +458,7 @@ end it "upgrade_addon(app_name, addon_name) with response" do - stub_api_request(:put, "/apps/example/addons/addon1"). + stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) expect(@client.upgrade_addon('example', 'addon1')). @@ -466,7 +466,7 @@ end it "downgrade_addon(app_name, addon_name) with response" do - stub_api_request(:put, "/apps/example/addons/addon1"). + stub_request(:put, "https://api.heroku.com/apps/example/addons/addon1"). to_return(:body => json_encode('price' => 'free', 'message' => "Don't Panic")) expect(@client.downgrade_addon('example', 'addon1')). @@ -474,7 +474,7 @@ end it "uninstall_addon(app_name, addon_name) with response" do - stub_api_request(:delete, "/apps/example/addons/addon1"). + stub_api_request(:delete, "/apps/example/addons/addon1?"). to_return(:body => json_encode('price'=> 'free', 'message'=> "Don't Panic")) expect(@client.uninstall_addon('example', 'addon1')). @@ -545,23 +545,4 @@ end end end - - describe "default_resource_options_for_uri" do - it "adds verify_ssl and ssl_ca_file for api.heroku.com" do - client = Heroku::Client.new("user", "password") - ssl_ca_file = client.send(:local_ca_file) - expect({ :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => ssl_ca_file}).to eq(client.send(:default_resource_options_for_uri, "https://api.heroku.com")) - end - - it "adds verify_ssl and ssl_ca_file for foo:bar@api.heroku.com" do - client = Heroku::Client.new("user", "password") - ssl_ca_file = client.send(:local_ca_file) - expect({ :verify_ssl => OpenSSL::SSL::VERIFY_PEER, :ssl_ca_file => ssl_ca_file}).to eq(client.send(:default_resource_options_for_uri, "https://foo:bar@api.heroku.com")) - end - - it "does not add verify_ssl and ssl_ca_file for others" do - client = Heroku::Client.new("user", "password") - expect({}).to eq(client.send(:default_resource_options_for_uri, "https://foo:bar@example.com")) - end - end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3c224a4f7..3ff832d65 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -2,7 +2,7 @@ if (Heroku::Helpers.running_on_windows?) $stdin = File.new("nul") -else +else $stdin = File.new("/dev/null") end @@ -21,7 +21,6 @@ require 'tmpdir' require "webmock/rspec" require "shellwords" -require "netrc" ENV['HEROKU_SKIP_ANALYTICS'] = '1' @@ -39,12 +38,7 @@ def org_api end def stub_api_request(method, path) - user, password = Netrc.read["api.heroku.com"] - if (user || password) - stub_request(method, Addressable::Template.new("https://{user}:{pass}@api.heroku.com#{path}")) - else - stub_request(method, "https://api.heroku.com#{path}") - end + stub_request(method, "https://api.heroku.com#{path}") end def prepare_command(klass) @@ -274,3 +268,4 @@ def self.version; 'heroku-cli/4.0.0-4f2c5c5 (amd64-darwin) go1.5' end config.before { Heroku::Helpers.error_with_failure = false } config.after { RR.reset } end + From 1d746f410b1fd83a2ab772662085764e3532b3c3 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Wed, 7 Sep 2016 16:56:53 -0500 Subject: [PATCH 944/952] v3.43.12 --- CHANGELOG | 4 ++++ Gemfile.lock | 2 +- lib/heroku/version.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 12fa86a92..4ce1cfe7f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.43.12 2016-09-07 +================== +Revert: Update rest-client gem because security vulnerabilities + 3.43.11 2016-09-07 ================== Update rest-client gem because security vulnerabilities diff --git a/Gemfile.lock b/Gemfile.lock index 46e08edd3..86186c6d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.11) + heroku (3.43.12) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 00afc9882..f8021e504 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.11" + VERSION = "3.43.12" end From d7e9718079b9936ffd264529bdd03a6734fd64cc Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Thu, 8 Sep 2016 14:16:58 -0500 Subject: [PATCH 945/952] Add appveyor test suite (#2013) --- Gemfile | 1 - Gemfile.lock | 8 -------- appveyor.yml | 13 +++++++++++++ spec/heroku/command/logs_spec.rb | 6 ++++++ spec/spec_helper.rb | 6 ++++-- 5 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 appveyor.yml diff --git a/Gemfile b/Gemfile index 70204ecb5..2125d3971 100644 --- a/Gemfile +++ b/Gemfile @@ -12,5 +12,4 @@ group :development, :test do gem "rspec" gem "webmock" gem "coveralls", :require => false - gem "pry" end diff --git a/Gemfile.lock b/Gemfile.lock index 86186c6d2..3930897e3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,7 +20,6 @@ GEM mime-types xml-simple builder (3.2.2) - coderay (1.1.0) coveralls (0.8.0) multi_json (~> 1.10) rest-client (>= 1.6.8, < 2) @@ -39,17 +38,12 @@ GEM json (1.8.2) launchy (2.4.3) addressable (~> 2.3) - method_source (0.8.2) mime-types (1.25.1) multi_json (1.11.2) net-ssh (2.9.2) net-ssh-gateway (1.2.0) net-ssh (>= 2.6.5) netrc (0.10.3) - pry (0.10.1) - coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) rake (10.4.2) rdoc (4.2.0) rest-client (1.6.8) @@ -76,7 +70,6 @@ GEM multi_json (~> 1.0) simplecov-html (~> 0.9.0) simplecov-html (0.9.0) - slop (3.6.0) term-ansicolor (1.3.0) tins (~> 1.0) thor (0.19.1) @@ -96,7 +89,6 @@ DEPENDENCIES heroku! json mime-types - pry rake rr rspec diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..6f701226c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,13 @@ +install: + - set PATH=C:\Ruby21\bin;%PATH% + - bundle install --deployment + +build: off + +before_test: + - ruby -v + - gem -v + - bundle -v + +test_script: + - bundle exec rake diff --git a/spec/heroku/command/logs_spec.rb b/spec/heroku/command/logs_spec.rb index 06a0eb500..c8b506feb 100644 --- a/spec/heroku/command/logs_spec.rb +++ b/spec/heroku/command/logs_spec.rb @@ -21,6 +21,12 @@ describe "with log output" do before(:each) do stub_core.read_logs("example", []).yields("2011-01-01T00:00:00+00:00 app[web.1]: test") + @term = ENV['TERM'] + ENV['TERM'] = 'xterm-256color' + end + + after(:each) do + ENV['TERM'] = @term end it "prettifies tty output" do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3ff832d65..a687ab9ba 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -8,8 +8,10 @@ require "rubygems" -require "coveralls" -Coveralls.wear! +unless ENV['APPVEYOR'] == 'True' + require "coveralls" + Coveralls.wear! +end require "excon" From 83265e0c362aece31e199e6e796d7e271f8dcec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Peignier?= Date: Mon, 10 Oct 2016 17:16:36 -0700 Subject: [PATCH 946/952] Stop assuming domain for some private endpoints (#2017) --- lib/heroku/client/heroku_postgresql.rb | 18 ++---------------- lib/heroku/client/heroku_postgresql_backups.rb | 17 ++--------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/lib/heroku/client/heroku_postgresql.rb b/lib/heroku/client/heroku_postgresql.rb index c636f4773..f820f4726 100644 --- a/lib/heroku/client/heroku_postgresql.rb +++ b/lib/heroku/client/heroku_postgresql.rb @@ -25,13 +25,9 @@ def initialize(attachment) def heroku_postgresql_host if attachment.starter_plan? - determine_host(ENV["HEROKU_POSTGRESQL_HOST"], "postgres-starter-api.heroku.com") + ENV["HEROKU_POSTGRESQL_HOST"] || "postgres-starter-api.heroku.com" else - if ENV['SHOGUN'] - "shogun-#{ENV['SHOGUN']}.herokuapp.com" - else - determine_host(ENV["HEROKU_POSTGRESQL_HOST"], "postgres-api.heroku.com") - end + ENV["HEROKU_POSTGRESQL_HOST"] || "postgres-api.heroku.com" end end @@ -219,16 +215,6 @@ def http_delete(path) sym_keys(json_decode(response.to_s)) end end - - private - - def determine_host(value, default) - if value.nil? - default - else - "#{value}.herokuapp.com" - end - end end module HerokuPostgresql diff --git a/lib/heroku/client/heroku_postgresql_backups.rb b/lib/heroku/client/heroku_postgresql_backups.rb index 237462911..97a86af8f 100644 --- a/lib/heroku/client/heroku_postgresql_backups.rb +++ b/lib/heroku/client/heroku_postgresql_backups.rb @@ -33,11 +33,7 @@ def transfers_public_url(id) end def heroku_postgresql_host - if ENV['SHOGUN'] - "shogun-#{ENV['SHOGUN']}.herokuapp.com" - else - determine_host(ENV["HEROKU_POSTGRESQL_HOST"], "postgres-api.heroku.com") - end + ENV["HEROKU_POSTGRESQL_HOST"] || "postgres-api.heroku.com" end def heroku_postgresql_resource @@ -45,8 +41,7 @@ def heroku_postgresql_resource "https://#{heroku_postgresql_host}/client/v11/apps", :user => Heroku::Auth.user, :password => Heroku::Auth.password, - :headers => self.class.headers - ) + :headers => self.class.headers) end def http_get(path) @@ -83,14 +78,6 @@ def display_heroku_warning(response) private - def determine_host(value, default) - if value.nil? - default - else - "#{value}.herokuapp.com" - end - end - def sym_keys(c) if c.is_a?(Array) c.map { |e| sym_keys(e) } From fd8ad9a8122e11dd2b312662dfc17bc19d0adaf4 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 28 Oct 2016 16:13:31 -0500 Subject: [PATCH 947/952] Monkey patch URI to split urls with addressable (#2031) * Monkey patch URI to split urls with addressable * Fixes bug where proxy url had underscore in it * Replace addressable with RFC3986_Parser from 2.3.1 --- Gemfile.lock | 2 +- lib/heroku/cli.rb | 1 + lib/heroku/uri.rb | 138 ++++++++++++++++++++++++++++++++++++++++ spec/heroku/uri_spec.rb | 11 ++++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 lib/heroku/uri.rb create mode 100644 spec/heroku/uri_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 3930897e3..db957eebd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,4 +95,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.12.5 + 1.13.6 diff --git a/lib/heroku/cli.rb b/lib/heroku/cli.rb index 045902414..ecd474655 100644 --- a/lib/heroku/cli.rb +++ b/lib/heroku/cli.rb @@ -12,6 +12,7 @@ require 'heroku/config' require 'heroku/analytics' require 'heroku/rollbar' +require 'heroku/uri' require 'json' class Heroku::CLI diff --git a/lib/heroku/uri.rb b/lib/heroku/uri.rb new file mode 100644 index 000000000..bc6fdbd1d --- /dev/null +++ b/lib/heroku/uri.rb @@ -0,0 +1,138 @@ +require 'uri' + +unless URI.const_defined?(:RFC3986_Parser) + # https://github.com/ruby/ruby/blob/v2_3_1/lib/uri/rfc3986_parser.rb + module URI + class RFC3986_Parser # :nodoc: + # URI defined in RFC3986 + # this regexp is modified not to host is not empty string + RFC3986_URI = /\A(?(?[A-Za-z][+\-.0-9A-Za-z]*):(?\/\/(?(?:(?(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?(?\[(?:(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{1,4}?::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?\d*))?)(?(?:\/(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?\/(?:(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g)*)?)|(?\g(?:\/\g)*)|(?))(?:\?(?[^#]*))?(?:\#(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/ + RFC3986_relative_ref = /\A(?(?\/\/(?(?:(?(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*)@)?(?(?\[(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{1,4}?::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:){,1}\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+)\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])+))?(?::(?\d*))?)(?(?:\/(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*))*)|(?\/(?:(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+)(?:\/\g)*)?)|(?(?(?:%\h\h|[!$&-.0-9;=@-Z_a-z~])+)(?:\/\g)*)|(?))(?:\?(?[^#]*))?(?:\#(?(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*))?)\z/ + attr_reader :regexp + + def initialize + @regexp = default_regexp.each_value(&:freeze).freeze + end + + def split(uri) #:nodoc: + begin + uri = uri.to_str + rescue NoMethodError + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + uri.ascii_only? or + raise InvalidURIError, "URI must be ascii only #{uri.dump}" + if m = RFC3986_URI.match(uri) + query = m["query".freeze] + scheme = m["scheme".freeze] + opaque = m["path-rootless".freeze] + if opaque + opaque << "?#{query}" if query + [ scheme, + nil, # userinfo + nil, # host + nil, # port + nil, # registry + nil, # path + opaque, + nil, # query + m["fragment".freeze] + ] + else # normal + [ scheme, + m["userinfo".freeze], + m["host".freeze], + m["port".freeze], + nil, # registry + (m["path-abempty".freeze] || + m["path-absolute".freeze] || + m["path-empty".freeze]), + nil, # opaque + query, + m["fragment".freeze] + ] + end + elsif m = RFC3986_relative_ref.match(uri) + [ nil, # scheme + m["userinfo".freeze], + m["host".freeze], + m["port".freeze], + nil, # registry, + (m["path-abempty".freeze] || + m["path-absolute".freeze] || + m["path-noscheme".freeze] || + m["path-empty".freeze]), + nil, # opaque + m["query".freeze], + m["fragment".freeze] + ] + else + raise InvalidURIError, "bad URI(is not URI?): #{uri}" + end + end + + def parse(uri) # :nodoc: + scheme, userinfo, host, port, + registry, path, opaque, query, fragment = self.split(uri) + scheme_list = URI.scheme_list + if scheme && scheme_list.include?(uc = scheme.upcase) + scheme_list[uc].new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + else + Generic.new(scheme, userinfo, host, port, + registry, path, opaque, query, + fragment, self) + end + end + + + def join(*uris) # :nodoc: + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + @@to_s = Kernel.instance_method(:to_s) + def inspect + @@to_s.bind(self).call + end + + private + + def default_regexp # :nodoc: + { + SCHEME: /\A[A-Za-z][A-Za-z0-9+\-.]*\z/, + USERINFO: /\A(?:%\h\h|[!$&-.0-;=A-Z_a-z~])*\z/, + HOST: /\A(?:(?\[(?:(?(?:\h{1,4}:){6}(?\h{1,4}:\h{1,4}|(?(?[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)\.\g\.\g\.\g))|::(?:\h{1,4}:){5}\g|\h{,4}::(?:\h{1,4}:){4}\g|(?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g|(?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g|(?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g|(?:(?:\h{1,4}:){,4}\h{1,4})?::\g|(?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}|(?:(?:\h{1,4}:){,6}\h{1,4})?::)|(?v\h+\.[!$&-.0-;=A-Z_a-z~]+))\])|\g|(?(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*))\z/, + ABS_PATH: /\A\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/, + REL_PATH: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])+(?:\/(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*)*\z/, + QUERY: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/, + FRAGMENT: /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~\/?])*\z/, + OPAQUE: /\A(?:[^\/].*)?\z/, + PORT: /\A[\x09\x0a\x0c\x0d ]*\d*[\x09\x0a\x0c\x0d ]*\z/, + } + end + + def convert_to_uri(uri) + if uri.is_a?(URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected URI object or URI string)" + end + end + + end # class Parser + end # module URI + + # http://stackoverflow.com/a/17108137/255982 + class URI::Parser + RFC3986_PARSER = URI::RFC3986_Parser.new + + def split(url) + RFC3986_PARSER.split(url) + end + end +end diff --git a/spec/heroku/uri_spec.rb b/spec/heroku/uri_spec.rb new file mode 100644 index 000000000..76183ff77 --- /dev/null +++ b/spec/heroku/uri_spec.rb @@ -0,0 +1,11 @@ +# encoding: utf-8 + +require "spec_helper" + +describe URI do + context "parse" do + it "should be monkeypatched to allow underscores in hosts" do + URI.parse("https://foo_bar.com") + end + end +end From e0bd57631806c05f049f0e0f3fda6efadb057916 Mon Sep 17 00:00:00 2001 From: Ransom Briggs Date: Fri, 28 Oct 2016 16:30:15 -0500 Subject: [PATCH 948/952] v3.43.13 --- CHANGELOG | 5 +++++ Gemfile.lock | 5 +---- lib/heroku/version.rb | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4ce1cfe7f..a398acdd4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +3.43.13 2016-10-28 +================== +Stop assuming domain for some private endpoints +Fixes bug where proxy url had underscore in it + 3.43.12 2016-09-07 ================== Revert: Update rest-client gem because security vulnerabilities diff --git a/Gemfile.lock b/Gemfile.lock index db957eebd..70220aa60 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.12) + heroku (3.43.13) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -93,6 +93,3 @@ DEPENDENCIES rr rspec webmock - -BUNDLED WITH - 1.13.6 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index f8021e504..99a12c881 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.12" + VERSION = "3.43.13" end From 444f6c0fc284db8f38d7b0bb379c2ff44c4d6a04 Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 11 Nov 2016 10:58:21 -0800 Subject: [PATCH 949/952] delete all windows clis older than 5.5 (#2034) --- lib/heroku/jsplugin.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/heroku/jsplugin.rb b/lib/heroku/jsplugin.rb index 3a828fea4..526340b23 100644 --- a/lib/heroku/jsplugin.rb +++ b/lib/heroku/jsplugin.rb @@ -274,8 +274,11 @@ def self.find_command(s) # check if release is one that isn't updateable def self.check_if_old - File.delete(bin) if windows? && setup? && version.start_with?("heroku-cli/4.24") - File.delete(bin) if setup? && version.start_with?("heroku-cli/4.27.5-") + return unless setup? + v = version.gsub(/heroku-cli\/([.\d]+)-.+/, '\1').chomp.split('.').map(&:to_i) + File.delete(bin) if windows? && v[0] < 5 # delete older than 5.x + File.delete(bin) if windows? && v[0] == 5 && v[1] < 5 # delete older than 5.5.x + File.delete(bin) if v == [4, 27, 5] rescue => e Rollbar.error(e) rescue From 16d21d238dd1957658ab005c8086136e2221d81d Mon Sep 17 00:00:00 2001 From: Jeff Dickey Date: Fri, 11 Nov 2016 10:59:25 -0800 Subject: [PATCH 950/952] v3.43.14 --- CHANGELOG | 4 ++++ Gemfile.lock | 5 ++++- lib/heroku/version.rb | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index a398acdd4..c4319af23 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ +3.43.14 2016-11-11 +================== +Fix windows autoupdates + 3.43.13 2016-10-28 ================== Stop assuming domain for some private endpoints diff --git a/Gemfile.lock b/Gemfile.lock index 70220aa60..e8acf93fe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - heroku (3.43.13) + heroku (3.43.14) heroku-api (= 0.4.2) launchy (= 2.4.3) multi_json (= 1.11.2) @@ -93,3 +93,6 @@ DEPENDENCIES rr rspec webmock + +BUNDLED WITH + 1.12.5 diff --git a/lib/heroku/version.rb b/lib/heroku/version.rb index 99a12c881..003f6e51a 100644 --- a/lib/heroku/version.rb +++ b/lib/heroku/version.rb @@ -1,3 +1,3 @@ module Heroku - VERSION = "3.43.13" + VERSION = "3.43.14" end From 729175a29e066948894c2477798c8cb280a7dc9a Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Mon, 28 Nov 2016 14:49:40 -0800 Subject: [PATCH 951/952] Reorganize proper cli description for pg:copy command --- lib/heroku/command/pg_backups.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index f4c4f1efa..783f22601 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -6,10 +6,11 @@ class Heroku::Command::Pg < Heroku::Command::Base # pg:copy SOURCE TARGET # + # copy all data from source database to target + # # --wait-interval SECONDS # how frequently to poll (to avoid rate-limiting) # - # copy all data from source database to target. At least one of - # these must be a Heroku Postgres database. + # At least one of these must be a Heroku Postgres database. # def copy source_db = shift_argument From 15ae4c3745b61c61aa9f240c037f80e4889a23c8 Mon Sep 17 00:00:00 2001 From: Kevin Huang Date: Mon, 28 Nov 2016 14:50:13 -0800 Subject: [PATCH 952/952] Add proper cli description for pg command on pg_backups.rb --- lib/heroku/command/pg_backups.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/heroku/command/pg_backups.rb b/lib/heroku/command/pg_backups.rb index 783f22601..628622ab1 100644 --- a/lib/heroku/command/pg_backups.rb +++ b/lib/heroku/command/pg_backups.rb @@ -3,6 +3,8 @@ require "heroku/command/base" require "heroku/helpers/heroku_postgresql" +# manage heroku-postgresql databases +# class Heroku::Command::Pg < Heroku::Command::Base # pg:copy SOURCE TARGET #

    k0XpMOj4^l$H4{+> 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�?!KisEbF&&ZfhdMA7ID$Fv<3#9D?3HC3) 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�<5xgsSKzB&77KqYrL-^N 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ⅈ`%C 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