Skip to content

Commit

Permalink
Ruby version installation errors and warnings
Browse files Browse the repository at this point in the history
Currently when a customer tries to upgrade their stack and are using an old version of Ruby they get a very unhelpful error:

```
       Command: 'set -o pipefail; curl -L --fail --retry 5 --retry-delay 1 --connect-timeout 3 --max-time 30 https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-1.9.3.tgz -s -o - | tar zxf - ' failed on attempt 1 of 3.
       Command: 'set -o pipefail; curl -L --fail --retry 5 --retry-delay 1 --connect-timeout 3 --max-time 30 https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-1.9.3.tgz -s -o - | tar zxf - ' failed on attempt 2 of 3.
 !
 !     An error occurred while installing ruby-1.9.3
 !
 !     Heroku recommends you use the latest supported Ruby version listed here:
 !     https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
 !
 !     For more information on syntax for declaring a Ruby version see:
 !     https://devcenter.heroku.com/articles/ruby-versions
 !
 !
 !     Debug InformationCommand: 'set -o pipefail; curl -L --fail --retry 5 --retry-delay 1 --connect-timeout 3 --max-time 30 https://s3-external-1.amazonaws.com/heroku-buildpack-ruby/heroku-18/ruby-1.9.3.tgz -s -o - | tar zxf - ' failed unexpectedly:
 !
 !     gzip: stdin: unexpected end of file
 !     tar: Child returned status 1
 !     tar: Error is not recoverable: exiting now
 !
 !     Push rejected, failed to compile Ruby app.
 !     Push failed
 ```

 This is awful and gives no context. The goal of this PR is to do a few things:

 - If a Ruby version does not exist on any stack, explicitly state the problem
 - If a Ruby version exists, but not on your current stack, list the stacks it is present on
 - If a Ruby version exists, but not for the next stack (i.e. heroku-16 if you're deploying to cedar-14) then the customer should get a warning
  • Loading branch information
schneems committed Feb 26, 2020
1 parent 1a15bf2 commit 643d408
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## Master (unreleased)

* Improve Ruby version download error messages (https://github.com/heroku/heroku-buildpack-ruby/pull/953)
* Update default Ruby version to 2.6.5 (https://github.com/heroku/heroku-buildpack-ruby/pull/947)

## v207 (12/16/2019)
Expand Down
1 change: 1 addition & 0 deletions lib/language_pack.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def self.detect(*args)
require "language_pack/helpers/rails_runner"
require "language_pack/helpers/bundler_wrapper"
require "language_pack/helpers/outdated_ruby_version"
require "language_pack/helpers/download_presence"
require "language_pack/installers/ruby_installer"
require "language_pack/installers/heroku_ruby_installer"
require "language_pack/installers/rbx_installer"
Expand Down
67 changes: 67 additions & 0 deletions lib/language_pack/helpers/download_presence.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# This class is used to check whether a binary exists on one or more stacks.
# The main motivation for adding this logic is to help people who are upgrading
# to a new stack if it does not have a given Ruby version. For example if someone
# is using Ruby 1.9.3 on the cedar-14 stack then they should be informed that it
# does not exist if they try to use it on the Heroku-18 stack.
#
# Example
#
# download = LanguagePack::Helpers::DownloadPresence.new(
# 'ruby-1.9.3.tgz',
# stacks: ['cedar-14', 'heroku-16', 'heroku-18']
# )
#
# download.call
#
# puts download.exists? #=> true
# puts download.valid_stack_list #=> ['cedar-14']
class LanguagePack::Helpers::DownloadPresence
STACKS = ['cedar-14', 'heroku-16', 'heroku-18']

def initialize(path, stacks: STACKS)
@path = path
@stacks = stacks
@fetchers = []
@threads = []
@stacks.each do |stack|
@fetchers << LanguagePack::Fetcher.new(LanguagePack::Base::VENDOR_URL, stack)
end
end

def next_stack(stack)
next_index = @stacks.index(stack) + 1

@stacks[next_index]
end

def exists_on_next_stack?(stack)
next_index = @stacks.index(stack) + 1
@threads[next_index]
end

def valid_stack_list
raise "not invoked yet, use the `call` method first" if @threads.empty?

@threads.map.with_index do |thread, i|
@stacks[i] if thread.value
end.compact
end

def exists?
raise "not invoked yet, use the `call` method first" if @threads.empty?

@threads.any? {|t| t.value }
end

def does_not_exist?
!exists?
end

def call
@fetchers.map do |fetcher|
@threads << Thread.new do
fetcher.exists?(@path)
end
end
end
end
3 changes: 1 addition & 2 deletions lib/language_pack/installers/heroku_ruby_installer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ def fetch_unpack(ruby_version, install_dir, build = false)
Dir.chdir(install_dir) do
file = "#{ruby_version.version_for_download}.tgz"
if build
ruby_vm = "ruby"
file.sub!(ruby_vm, "#{ruby_vm}-build")
file.sub!("ruby", "ruby-build")
end
@fetcher.fetch_untar(file)
end
Expand Down
96 changes: 52 additions & 44 deletions lib/language_pack/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -442,10 +442,32 @@ def warn_outdated_ruby

warn_outdated_minor
warn_outdated_eol
warn_stack_upgrade
true
end
end

def warn_stack_upgrade
return if @ruby_download_check.next_stack(stack).nil?
return unless @ruby_download_check.exists_on_next_stack?(stack)

warn(<<~WARNING)
Your Ruby version is not present on the next stack
You are currently using #{ruby_version.version_for_download} on #{stack} stack.
This version does not exist on
#{@ruby_download_check.next_stack(stack)}. In order to upgrade your stack you will
need to upgrade to a supported Ruby version.
For a list of supported Ruby versions see:
https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
For a list of the oldest Ruby versions present on a given stack see:
https://devcenter.heroku.com/articles/ruby-support#oldest-available-runtimes
WARNING
end

def warn_outdated_eol
return unless @outdated_version_check.maybe_eol?

Expand Down Expand Up @@ -512,10 +534,15 @@ def install_ruby
instrument 'ruby.install_ruby' do
return false unless ruby_version
installer = LanguagePack::Installers::RubyInstaller.installer(ruby_version).new(@stack)
file = "#{ruby_version.version_for_download}.tgz"

if ruby_version.build?
installer.fetch_unpack(ruby_version, build_ruby_path, true)
file.sub!("ruby", "ruby-build")
end
@ruby_download_check = LanguagePack::Helpers::DownloadPresence.new(file)
@ruby_download_check.call

installer.install(ruby_version, slug_vendor_ruby)

@outdated_version_check = LanguagePack::Helpers::OutdatedRubyVersion.new(
Expand All @@ -538,61 +565,42 @@ def install_ruby
end

true
rescue LanguagePack::Fetcher::FetchError => error
if stack == "heroku-18" && ruby_version.version_for_download.match?(/ruby-2\.(2|3)/)
message = <<ERROR
An error occurred while installing #{ruby_version.version_for_download}
This version of Ruby is not available on Heroku-18. The minimum supported version
of Ruby on the Heroku-18 stack can found at:
https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
ERROR
rescue LanguagePack::Fetcher::FetchError
if @ruby_download_check.does_not_exist?
message = <<~ERROR
The Ruby version you are trying to install does not exist: #{ruby_version.version_for_download}
ERROR
else
message = <<~ERROR
The Ruby version you are trying to install does not exist on this stack.
ci_message = <<ERROR
You are trying to install #{ruby_version.version_for_download} on #{stack}.
If you did not intend to build your app for CI on the Heroku-18 stack
please set your stack version manually in the `app.json`:
Ruby #{ruby_version.version_for_download} is present on the following stacks:
```
"stack": "heroku-16"
```
- #{@ruby_download_check.valid_stack_list.join("\n - ")}
ERROR

More information about this change in behavior can be found at:
https://help.heroku.com/3Y1HEXGJ/why-doesn-t-ruby-2-3-7-work-in-my-ci-tests
if env("CI")
message << <<~ERROR
ERROR
On Heroku CI you can set your stack in the `app.json`. For example:
if env("CI")
mcount "fail.bad_version_fetch.heroku-18.ci"
message << ci_message
else
mcount "fail.bad_version_fetch.heroku-18"
```
"stack": "heroku-16"
```
ERROR
end

error message
end

mcount "fail.bad_version_fetch"
mcount "fail.bad_version_fetch.#{ruby_version.version_for_download}"
message = <<ERROR
An error occurred while installing #{ruby_version.version_for_download}
Heroku recommends you use the latest supported Ruby version listed here:
https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
For more information on syntax for declaring a Ruby version see:
https://devcenter.heroku.com/articles/ruby-versions
message << <<~ERROR
ERROR

if ruby_version.jruby?
message << "Note: Only JRuby 1.7.13 and newer are supported on Cedar-14"
end
Heroku recommends you use the latest supported Ruby version listed here:
https://devcenter.heroku.com/articles/ruby-support#supported-runtimes
message << "\nDebug Information"
message << error.message
For more information on syntax for declaring a Ruby version see:
https://devcenter.heroku.com/articles/ruby-versions
ERROR

error message
end
Expand Down
66 changes: 66 additions & 0 deletions spec/helpers/download_presence_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require "spec_helper"

describe LanguagePack::Helpers::DownloadPresence do
it "knows if exists on the next stack" do
download = LanguagePack::Helpers::DownloadPresence.new(
'ruby-1.9.3.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18']
)

download.call

expect(download.next_stack("cedar-14")).to eq("heroku-16")
expect(download.next_stack("heroku-16")).to eq("heroku-18")
expect(download.next_stack("heroku-18")).to be_falsey

expect(download.exists_on_next_stack?("cedar-14")).to be_truthy
end


it "detects when a package is not present on higher stacks" do
download = LanguagePack::Helpers::DownloadPresence.new(
'ruby-1.9.3.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18']
)

download.call

expect(download.exists?).to eq(true)
expect(download.valid_stack_list).to eq(['cedar-14'])
end

it "detects when a package is present on two stacks but not a third" do
download = LanguagePack::Helpers::DownloadPresence.new(
'ruby-2.3.0.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18']
)

download.call

expect(download.exists?).to eq(true)
expect(download.valid_stack_list).to eq(['cedar-14', 'heroku-16'])
end

it "detects when a package does not exist" do
download = LanguagePack::Helpers::DownloadPresence.new(
'does-not-exist.tgz',
stacks: ['cedar-14', 'heroku-16', 'heroku-18']
)

download.call

expect(download.exists?).to eq(false)
expect(download.valid_stack_list).to eq([])
end

it "detects default ruby version" do
download = LanguagePack::Helpers::DownloadPresence.new(
"#{LanguagePack::RubyVersion::DEFAULT_VERSION}.tgz",
)

download.call

expect(download.exists?).to eq(true)
expect(download.valid_stack_list).to include(LanguagePack::Helpers::DownloadPresence::STACKS.last)
end
end

0 comments on commit 643d408

Please sign in to comment.